大数据 — 小白讲解 + 面试题精解
大数据 — 小白讲解 + 面试题精解
定位:假设你只会写 SQL 单机查询,所有概念从零讲起,配代码示例 + 面试题。 覆盖:Apache Spark → Apache HBase 两大模块。 用法:先读讲解理解概念 → 跑代码验证 → 再看面试题自测。 特色:每个知识点都结合简历真实项目案例,面试时能直接用。
目录
第一章:Apache Spark(简历项目一资金拆分)
简历直接写了“将 Oracle 存储过程拆分规则转义为 Spark SQL 并行计算,拆分耗时 30+ 分钟降至 3 分钟”。 面试官会重点问:Spark 为什么快、Shuffle 和数据倾斜。
1.1 Spark 是什么(为什么比存储过程快)
小白讲解
一句话:Spark 是分布式内存计算引擎,把大数据拆成很多小块,分到多台机器并行算。
Spark 的核心抽象:
RDD(弹性分布式数据集)
最底层抽象,不可变的分布式数据集合
可以理解为"分布在多台机器上的一个大数组"
DataFrame(重点,简历用的这个)
RDD 之上的结构化抽象,带列名和类型,类似"分布式的大表"
支持 SQL 查询(Spark SQL)
Spark SQL(简历直接写了)
用 SQL 语法操作 DataFrame
把 SQL 翻译成分布式执行计划
Spark 为什么比 Oracle 存储过程快?
存储过程(串行):
一个存储过程在数据库单节点上跑
游标一行行处理 → 串行 + 磁盘 IO → 慢
Spark SQL(并行):
数据拆成 N 个分区 → N 个 Executor 并行计算
中间结果尽量在内存 → 减少磁盘 IO
→ 30 分钟的任务 3 分钟跑完
结合简历案例
简历原文:
"将原 Oracle 存储过程中的拆分规则转义为 Spark SQL 并行计算,
设计合理分区策略避免 Shuffle 数据倾斜;
拆分执行耗时从 30+ 分钟降至 3 分钟以内,日终清算窗口缩短 80%"
面试话术:
"项目一的资金拆分逻辑原来写在 Oracle 存储过程里,
用游标一行行遍历处理,千万级数据要跑 30 多分钟,
严重影响日终清算窗口。
我把拆分规则用 Spark SQL 重写:
1. 游标循环 → Spark SQL 窗口函数(ROW_NUMBER / SUM OVER)
2. 临时表 → Spark DataFrame
3. 条件分支 → CASE WHEN
并行化后,数据按指令类型、业务日期分区,
多个 Executor 并行计算,30 分钟降到 3 分钟以内。"
1.2 RDD / DataFrame / Dataset
小白讲解
三个抽象的关系(从底层到高层):
RDD(最低层)
- 无类型,只有泛型 RDD[T]
- 操作是函数式的(map/filter/reduce)
- 适合底层优化,但写起来繁琐
DataFrame(简历用的)
- 有 schema(列名 + 类型),类似表
- 可以写 SQL
- 有 Catalyst 优化器自动优化
Dataset(最高层,强类型)
- DataFrame + 类型安全
- Java/Scala 泛型
面试建议:说清楚"DataFrame 是带 schema 的 RDD,能写 SQL 且带优化器"
代码示例
// Spark SQL 资金拆分(简化版)
SparkSession spark = SparkSession.builder()
.appName("FundSplit")
.getOrCreate();
// 读取指令数据(从 HDFS 或 HBase)
Dataset<Row> instructions = spark.read()
.format("jdbc")
.option("url", "jdbc:gaussdb://...")
.option("dbtable", "instruction")
.load();
// 用 Spark SQL 实现拆分规则(替代 Oracle 游标逻辑)
instructions.createOrReplaceTempView("instruction");
Dataset<Row> splitResult = spark.sql(
"SELECT " +
" instruction_no, " +
" biz_date, " +
" amount, " +
" -- 用窗口函数替代游标累计:按业务日期分区,按金额排序累计" +
" SUM(amount) OVER (PARTITION BY biz_date ORDER BY instruction_no) AS cumsum, " +
" -- 拆分成主从指令:金额超阈值拆主从" +
" CASE WHEN amount > 1000000 THEN 'PRIMARY' ELSE 'SUB' END AS split_type " +
"FROM instruction " +
"WHERE status = 'APPROVED'"
);
// 按拆分类型分区写入(合理分区避免 Shuffle 倾斜)
splitResult.repartition(200) // 控制分区数
.write()
.mode("overwrite")
.format("parquet")
.save("/result/split");
面试题
Q1:RDD、DataFrame、Dataset 的区别?(高频 ⭐⭐⭐⭐)
答:
- RDD:最低层抽象,无 schema,函数式操作,无优化器
- DataFrame:带 schema(列名+类型)的分布式表,可写 SQL,有 Catalyst 优化器
- Dataset:DataFrame + 强类型(编译期类型检查)
- 简历用 DataFrame + Spark SQL(有 schema + 能写 SQL + 自动优化)
Q2:Spark 的惰性计算是什么?(中频 ⭐⭐⭐)
答:transformation(map/filter/select)不会立即执行,只是记录执行计划;遇到 action(count/collect/save)才真正触发计算。好处是 Spark 可以在执行前做整体优化(如合并多个 filter、裁剪不需要的列)。
1.3 Shuffle 原理(面试必问)
小白讲解
一句话:Shuffle = 数据在不同节点间重新分区(洗牌),是 Spark 最耗性能的操作。
什么时候触发 Shuffle?
需要把不同分区的数据按某个 key 重新分组时:
- groupBy / reduceByKey
- join(两个表关联)
- repartition(重新分区)
- 窗口函数(PARTITION BY)
Shuffle 为什么慢?
1. 磁盘 IO:中间数据要落盘(写入 shuffle 文件)
2. 网络 IO:数据在节点间传输
3. 数据倾斜:某个 key 数据特别多,全压到一个节点
Shuffle 的过程(以 reduceByKey 为例):
① Map 阶段:每个分区产出 (key, value)
② Shuffle 写:按 key 的哈希值分区,写到磁盘文件
③ Shuffle 读:Reducer 拉取自己负责的分区数据
④ Reduce 阶段:按 key 聚合
面试题
Q1:什么是 Shuffle?为什么耗性能?(高频 ⭐⭐⭐⭐)
答:
- Shuffle 是数据跨节点重新分区的过程(按 key 哈希重新分布)
- 耗性能因为:①中间数据落盘(磁盘 IO)②跨节点传输(网络 IO)③可能数据倾斜
- 优化:减少 Shuffle 次数、用广播变量代替小表 join、合理分区
Q2:哪些操作会触发 Shuffle?(中频 ⭐⭐⭐)
答:groupBy/reduceByKey、join、repartition、distinct、窗口函数(PARTITION BY)。而 map/filter/select 这类窄依赖操作不触发 Shuffle。
1.4 数据倾斜(简历直接写了)
小白讲解
数据倾斜:
数据按 key 哈希分区时,某个 key 的数据量特别大
→ 该 key 所在的分区任务特别慢(其他任务都跑完了,它还在跑)
→ 整体任务被拖慢(木桶效应)
举例:
按指令类型分区,99% 的指令都是"普通"类型
→ "普通"类型的数据全在一个分区
→ 那个分区要处理 99% 的数据,其他分区处理 1%
→ 严重的负载不均
解决方案:
1. 加盐(salting):
给 key 加随机前缀,打散到多个分区
(如 key 从 "普通" 变成 "普通_0"、"普通_1" ... "普通_9")
聚合后再去掉前缀做二次聚合
2. 自定义分区器:
针对热点 key 单独处理
3. 两阶段聚合:
局部聚合(加盐)→ 全局聚合(去盐)
4. 广播 join:
小表用 broadcast,避免 shuffle join
结合简历案例
简历原文:
"设计合理分区策略避免 Shuffle 数据倾斜"
面试话术:
"资金拆分时发现某类指令(如普通转账)占了 99% 的数据量,
按指令类型分区会导致严重倾斜——那个分区跑 10 分钟,
其他分区 10 秒就完了。
我做了两个优化:
1. 加盐:对热点类型加随机后缀(普通_0 ~ 普通_9),
把热点数据打散到多个分区,先局部聚合
2. 去盐二次聚合:把加盐前缀去掉后再做全局聚合
最终各分区负载均衡,整体耗时大幅下降。"
代码示例
// 加盐解决数据倾斜
Dataset<Row> instructions = spark.sql("SELECT type, amount FROM instruction");
// 1. 加盐:热点 key 加随机后缀
Dataset<Row> salted = instructions
.withColumn("salt", functions.rand() % 10) // 随机 0~9
.withColumn("salted_key", functions.concat(functions.col("type"),
functions.lit("_"), functions.col("salt")))
.groupBy("salted_key")
.agg(functions.sum("amount").as("partial_sum"));
// 2. 去盐:去掉后缀,二次聚合
Dataset<Row> result = salted
.withColumn("type", functions.split(functions.col("salted_key"), "_").getItem(0))
.groupBy("type")
.agg(functions.sum("partial_sum").as("total_amount"));
面试题
Q1:Spark 数据倾斜怎么解决?(高频 ⭐⭐⭐⭐⭐)
简历直接写了,必须会答!
答:
- 加盐(salting):热点 key 加随机前缀打散到多个分区,聚合后去盐二次聚合(简历方案)
- 自定义分区器:热点 key 单独处理
- 广播 join:小表广播到所有节点,避免 shuffle join
- 提高并行度:增加分区数(治标不治本)
- 预聚合:能预聚合的先聚合,减少 shuffle 数据量
Q2:怎么判断有没有数据倾斜?(中频 ⭐⭐⭐)
答:
- 看 Spark UI:某个 Stage 的某个 task 耗时远超其他 task
- 看 task 处理的数据量:某个 task 读取的数据量是其他的几十倍
- 看日志:某个 task 长期 RUNNING,其他 task 都 FINISHED 了
第二章:Apache HBase(简历项目一中间结果存储)
简历直接写了“拆分中间结果迁移至 HBase,设计 RowKey 前缀策略(业务日期+指令ID)支持范围查询”。 面试官会重点问:LSM 树原理、RowKey 设计。
2.1 LSM 树原理(简历直接写了)
小白讲解
一句话:LSM 树把随机写变成顺序写,通过“先写内存、再批量刷盘”大幅提升写入性能。
LSM 树 vs B+ 树(面试重点对比):
B+ 树(MySQL InnoDB 用的):
写入时:直接写磁盘上的数据页,随机 IO
→ 写入慢(每次写都要定位到磁盘位置)
→ 适合读多写少的场景
LSM 树(HBase/LevelDB/RocksDB 用的):
写入时:先写内存(MemStore),满了再顺序刷盘(HFile)
→ 写入快(顺序写,避免随机 IO)
→ 适合写多读少的场景
LSM 树的写入流程(面试要能画出来):
① 写请求 → 先写 WAL(Write-Ahead Log,预写日志,防丢失)
② 数据写入内存 MemStore
③ MemStore 达到阈值 → Flush 刷盘为 HFile(顺序写,很快)
④ HFile 越来越多 → Compaction 合并(整理碎片)
LSM 树为什么写入快?
随机写 → 顺序写
B+ 树每次写都要寻道(随机 IO,慢)
LSM 树写内存 + 顺序刷盘(顺序 IO,快一个数量级)
结合简历案例
简历原文:
"利用 HBase 的 LSM 树结构提升大批量写入性能"
面试话术:
"资金拆分会产生海量中间结果(每天千万级),
如果用 MySQL 存,随机写入性能扛不住。
HBase 的 LSM 树结构把写入变成:
先写内存 MemStore → 满了顺序刷盘为 HFile,
顺序写比 MySQL 的随机写快一个数量级。
所以我把拆分中间结果放到 HBase,大批量写入性能大幅提升。"
面试题
Q1:什么是 LSM 树?为什么写入快?(高频 ⭐⭐⭐⭐)
简历直接写了,必须会答!
答:
- LSM 树(Log-Structured Merge Tree)用“顺序写”替代“随机写”
- 写入流程:先写 WAL → 写内存 MemStore → 满了顺序刷盘为 HFile
- 写入快的原因:B+ 树每次写都要随机寻道,LSM 树写内存 + 顺序刷盘,顺序 IO 比随机 IO 快一个数量级
- 代价:读要查 MemStore + 多个 HFile,读性能不如 B+ 树(所以 HBase 适合写多读少)
Q2:LSM 树和 B+ 树的区别?(高频 ⭐⭐⭐⭐)
| 对比项 | B+ 树(MySQL) | LSM 树(HBase) |
|---|---|---|
| 写入 | 随机写,慢 | 顺序写,快 |
| 读取 | 直接查,快 | 查多文件合并,慢 |
| 适用 | 读多写少 | 写多读少 |
| 代表 | MySQL InnoDB | HBase/LevelDB/RocksDB |
2.2 RowKey 设计(简历直接写了)
小白讲解
RowKey 为什么重要?
HBase 按 RowKey 的字典序存储数据
相近的 RowKey 存在同一个 Region(数据分片)
好的 RowKey 设计能:
1. 支持范围扫描(前缀相同的 RowKey 相邻)
2. 避免热点(数据均匀分布到各 Region)
RowKey 设计原则:
1. 长度:尽量短(减少存储和内存)
2. 散列:避免用连续递增的时间戳/ID 做开头(会集中到一个 Region)
3. 前缀:把查询常用的字段放前面,支持前缀范围扫描
简历的 RowKey:业务日期 + 指令ID
RowKey = 20260115 + 00123456
→ 查询某一天的指令:scan 20260115 前缀,范围扫描
→ 业务日期放前面,因为查询都按日期过滤
结合简历案例
简历原文:
"设计 RowKey 前缀策略(业务日期+指令ID)支持范围查询"
面试话术:
"HBase 按 RowKey 字典序存储,我把 RowKey 设计成:
业务日期(8位)+ 指令ID(8位)
好处:
1. 范围查询:查某一天的指令,scan '20260115' 前缀,
直接范围扫描,不用全表扫
2. 写入均匀:指令ID 是散列的,避免数据集中到一个 Region
注意点:不能用时间戳做 RowKey 开头,
因为时间戳连续递增会导致数据都写到一个 Region(热点问题)。"
面试题
Q1:HBase 的 RowKey 怎么设计?(高频 ⭐⭐⭐⭐⭐)
简历直接写了,必须会答!
答:三个原则:
- 长度短:减少存储和内存开销
- 散列分布:避免热点。不要用连续递增的时间戳/自增ID做开头,可以反转或加盐
- 前缀支持范围查询:把高频查询字段放前面(如业务日期+指令ID,scan 前缀即可范围查询)
Q2:HBase 热点问题(Hotspotting)是什么?(中频 ⭐⭐⭐)
答:如果 RowKey 开头是连续递增的(如时间戳),新写入的数据都会落到同一个 Region,导致该 Region 过载,其他 Region 空闲。解决:RowKey 加盐(随机前缀)、反转时间戳、或哈希打散。
2.3 HBase 架构与写入流程
小白讲解
HBase 架构:
HMaster:管理 Region 的分配和负载均衡
RegionServer:管理 Region,处理读写请求
Region:数据分片(按 RowKey 范围划分)
Store:一个列族的存储(MemStore + HFile)
WAL:预写日志(Write-Ahead Log)
写入流程(面试必问):
① 写 WAL(先记日志,防数据丢失)
② 写 MemStore(内存)
③ MemStore 满了 → Flush 成 HFile(磁盘,顺序写)
④ HFile 多了 → Compaction 合并(整理碎片,删除过期数据)
面试题
Q1:HBase 的写入流程?(高频 ⭐⭐⭐⭐)
答:写 WAL → 写 MemStore(内存)→ Flush 刷 HFile(磁盘)→ Compaction 合并。WAL 保证数据不丢失(崩溃后可恢复),MemStore 保证写入快(先写内存),Flush 是顺序写(快),Compaction 清理碎片。
Q2:HBase 为什么适合大批量写入?(中频 ⭐⭐⭐)
答:LSM 树结构把写入变成“写内存 + 顺序刷盘”,避免了 B+ 树的随机写。适合写多读少的场景(如时序数据、日志、中间结果存储)。简历就是把资金拆分中间结果放 HBase,利用其高写入性能。
面试速查表(大数据)
| 考点 | 一句话答案 |
|---|---|
| Spark 为什么快 | 分布式内存计算,多 Executor 并行 |
| RDD vs DataFrame | DataFrame 带 schema,能写 SQL,有优化器 |
| Shuffle | 数据跨节点重新分区,落盘+网络+可能倾斜 |
| 数据倾斜 | 热点 key 集中,加盐打散+二次聚合 |
| LSM 树 | 顺序写替代随机写,写内存+顺序刷盘 |
| LSM vs B+ 树 | LSM 写快读慢,B+ 树读快写慢 |
| RowKey 设计 | 短+散列防热点+前缀支持范围查询 |
| 热点问题 | 连续递增开头→集中一个 Region,加盐/反转解决 |
| HBase 写入流程 | WAL→MemStore→Flush→Compaction |
最后更新:2026-08-19