这是一个非常核心且实际的大数据问题。大数据去重与单机去重有本质区别,核心在于数据量远超单机内存和计算能力,因此必须采用分布式、可扩展的方案。
下面我将从核心思想、常用技术、具体实现方案和优化技巧四个方面,详细解释大数据如何对数据去重。
一、核心思想
大数据去重的核心思想是 “分而治之”:
- 分片: 将海量数据集分割成多个小块(分片)。
- 排序/聚合: 在每个小块内部或在跨块的过程中进行排序、聚合或标识。
- 合并: 将处理后的结果合并,并在此过程中剔除重复记录。
关键在于,这个过程的每一步都必须在分布式集群上并行执行,才能处理TB/PB级别的数据。
二、常用技术与框架
大数据去重通常在以下框架中实现:
- 批处理场景:
- Apache Spark: 目前最主流的方案,凭借其内存计算和强大的DAG调度器,性能优异。
- Apache Flink: 同样强大的批流一体引擎,在批处理去重上也有很好表现。
- Apache Hive: 基于Hadoop MapReduce,通过SQL语句实现,稳定但速度相对较慢。
- 流处理场景:
- Apache Flink: 在实时数据流中去重的首选,提供精确一次(Exactly-Once)语义的状态保障。
- Apache Spark Streaming: 微批处理模式,也可用于准实时去重。
三、具体实现方案与代码示例
我们以最常用的 Spark 和 Hive SQL 为例,介绍几种典型的去重方法。
方案1:使用 distinct() 算子(全字段去重)
这是最简单直接的方法,适用于基于整行数据完全一样才算重复的场景。
Spark Scala 示例:
val rawData = spark.read.parquet("hdfs://path/to/your/data")
val distinctData = rawData.distinct()
distinctData.write.parquet("hdfs://path/to/distinct_data")
Hive SQL 示例:
INSERT OVERWRITE TABLE distinct_table
SELECT DISTINCT * FROM raw_table;
工作原理:
Spark会将所有字段作为Key,进行Shuffle(数据混洗),将相同Key的数据分发到同一个计算节点上,然后在该节点上保留唯一的一条记录。缺点: Shuffle过程网络和IO开销大,性能最差。
方案2:使用 dropDuplicates() 算子(指定字段去重)
更常用的场景是根据某几个字段的组合来判断是否重复。例如,根据用户ID和时间戳去重。
Spark Scala 示例:
val rawData = spark.read.parquet("hdfs://path/to/your/data")
// 根据指定的列进行去重
val distinctData = rawData.dropDuplicates("user_id", "timestamp")
distinctData.write.parquet("hdfs://path/to/distinct_data")
Hive SQL 示例(使用 ROW_NUMBER() 窗口函数):
这是Hive中最常用且高效的去重方法。
INSERT OVERWRITE TABLE distinct_table
SELECT
user_id,
timestamp,
other_columns
FROM (
SELECT
*,
ROW_NUMBER() OVER (PARTITION BY user_id, timestamp ORDER BY proc_time DESC) as rn
FROM raw_table
) t
WHERE rn = 1;
PARTITION BY指定了去重的键。ORDER BY用于在重复数据中定义保留哪一条(例如,保留最新proc_time的那条)。
工作原理:
dropDuplicates 和 ROW_NUMBER 方法都需要进行Shuffle。它们根据你指定的Key进行分区,然后在每个分区内部处理重复数据。ROW_NUMBER 方法更灵活,可以控制保留策略。
方案3:使用聚合函数(groupBy)
适合在去重的同时,还需要进行一些聚合操作(如取最大值、求和等)。
Spark Scala 示例:
val rawData = spark.read.parquet("hdfs://path/to/your/data")
// 根据 user_id 分组,并取每个user_id最新的一条记录(假设有update_time字段)
val distinctData = rawData
.groupBy("user_id")
.agg(max(struct($"update_time", $"other_columns")).as("latest"))
.select("latest.*") // 将struct展开
distinctData.write.parquet("hdfs://path/to/distinct_data")
Hive SQL 示例:
INSERT OVERWRITE TABLE distinct_table
SELECT
user_id,
max(update_time) as latest_update_time,
first_value(other_column) -- 通过first_value获取对应最新时间的其他字段
FROM raw_table
GROUP BY user_id;
工作原理: 同样需要Shuffle,根据分组键将数据分发,然后在每个组内进行聚合计算。
四、高级与优化技巧
面对超大规模数据,简单的去重可能仍然会遇到性能瓶颈。以下是一些进阶方法:
1. 使用布隆过滤器 - 预处理去重
场景: 在海量数据中快速判断某条记录可能已经存在,常用于预处理阶段,过滤掉大部分明显重复的数据。
原理: 布隆过滤器是一种概率型数据结构,占用空间极小,可以高效地判断一个元素“一定不存在”或“可能存在”于一个集合中。
Spark 示例:
// 假设有一个已知的已去重数据集 smallSet,我们用它来构建布隆过滤器
val bloomFilter = smallSet.stat.bloomFilter("key_column", expectedNumItems = 1000000, fpp = 0.01)
// 用这个过滤器去过滤海量数据集 largeSet
val filteredData = largeSet.filter(row => !bloomFilter.mightContain(row.getAs[String]("key_column")))
// filteredData 中包含了“可能”是新的数据,可以再结合精确去重方法进行最终处理
2. 分桶表
场景: 如果经常需要按某个字段(如用户ID)进行去重,可以将数据表创建为分桶表。
原理: 在数据存储时,就根据指定的桶列和桶数量,将数据哈希到不同的文件中。这样,在后续进行 GROUP BY 或 DISTINCT 操作时,Spark/Hive可以避免全量的Shuffle,因为相同Key的数据已经物理上存储在同一个桶文件里了,大大提升了性能。
Hive/Spark SQL 示例:
-- 创建分桶表
CREATE TABLE bucketed_table (
user_id INT,
data STRING
)
CLUSTERED BY (user_id) INTO 128 BUCKETS
STORED AS PARQUET;
-- 插入数据时,需要设置属性以启用分桶
SET hive.enforce.bucketing = true;
INSERT INTO bucketed_table SELECT * FROM raw_table;
-- 对分桶表进行去重,效率极高
SELECT user_id, count(*) FROM bucketed_table GROUP BY user_id;
3. 利用数据湖格式的“ Upsert ”功能
场景: 需要对不断变化的数据集进行增量去重和更新。
原理: 使用支持ACID事务的数据湖格式,如 Apache Hudi、Delta Lake 或 Apache Iceberg。它们提供了 UPSERT(更新插入)操作,可以非常高效地根据主键来合并新数据,如果存在则更新,不存在则插入,这本质上就是一种高效的、可回溯的去重过程。
Delta Lake (Spark) 示例:
// 将新数据merge到已有表中
deltaTable
.as("target")
.merge(newData.as("source"), "target.user_id = source.user_id")
.whenMatched().updateAll() // 如果匹配到,更新所有字段
.whenNotMatched().insertAll() // 如果没匹配到,插入整条数据
.execute()
总结
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
distinct() | 整行完全重复 | 简单直观 | 性能最差,Shuffle开销大 |
dropDuplicates() / ROW_NUMBER() | 按指定字段去重 | 灵活,可指定字段和保留策略 | 需要Shuffle,数据量大时慢 |
groupBy 聚合 | 去重同时需聚合 | 功能强大,一箭双雕 | 需要Shuffle,逻辑可能复杂 |
| 布隆过滤器 | 超大规模数据预处理 | 内存占用小,查询极快 | 有误判率,是概率性去重 |
| 分桶表 | 频繁按某字段去重 | 避免Shuffle,性能极高 | 需要预先规划并创建表 |
| 数据湖格式 | 增量、更新式去重 | 支持ACID,高效Upsert | 架构相对复杂 |
选择建议:
- 常规去重: 首选
dropDuplicates或 Hive的ROW_NUMBER()窗口函数。 - 性能瓶颈: 如果数据量巨大导致Shuffle过慢,考虑使用布隆过滤器进行预处理,或者将表设计为分桶表。
- 流式去重/增量更新: 使用 Flink 的键控状态(Keyed State)或 数据湖格式(Hudi/Delta/Iceberg) 的
UPSERT功能。

1300

被折叠的 条评论
为什么被折叠?



