3、哈希去重原理:哈希函数基础,MD5/SHA-1/SHA-256对比,哈希碰撞与解决方案
哈希去重,说白了就是给每条数据算一个「指纹」。指纹相同,数据就重复。这个思路简单,但背后的门道不少。我最早接触哈希去重是在做日志清洗的时候,几亿条日志用字符串比对根本跑不动,换成哈希后速度直接起飞。但后来也踩过坑——嗯,今天咱们就把这些坑一个个填上。
3.1 哈希函数基础
哈希函数是个数学映射。输入任意长度的数据,输出固定长度的摘要。这个摘要就是哈希值。
好的哈希函数有几个关键特性:
- 确定性:同样的输入,永远得到同样的输出。
- 快速计算:几微秒内算出结果。
- 雪崩效应:输入改一个比特,输出大变样。
- 抗碰撞性:很难找到两个不同输入产生相同输出。
举个例子,你输入「hello」和「Hello」,哈希值会完全不同。这就是雪崩效应在起作用。
3.2 MD5 / SHA-1 / SHA-256 对比
这三个是哈希去重里最常见的算法。我一个个说。
MD5
MD5 输出 128 位,32 个十六进制字符。计算速度极快,每秒能算几百万次。但它的安全性早就被攻破了。2004 年就有研究者找到了碰撞。
我的建议:MD5 只能用在非安全场景,比如内部测试、临时去重。生产环境?别用。
SHA-1
SHA-1 输出 160 位,40 个十六进制字符。比 MD5 慢一点,但也没慢多少。2017 年 Google 公开了 SHA-1 碰撞实例,两个不同的 PDF 文件产生了相同的 SHA-1 值。
避坑指南:我曾经在一个旧系统里看到 SHA-1 做数据指纹,当时觉得问题不大。后来数据量上亿后,真撞上了。排查了两天才定位到是哈希碰撞。从那以后,我所有新项目都只用 SHA-256。
SHA-256
SHA-256 输出 256 位,64 个十六进制字符。属于 SHA-2 家族。目前没有公开的碰撞攻击。计算速度比前两个慢,但对去重场景来说完全够用。
为什么慢反而好?你想想看,攻击者想伪造碰撞也更难。安全性和性能之间,我选安全性。
| 算法 | 输出长度 | 安全性 | 速度 | 推荐场景 |
|---|---|---|---|---|
| MD5 | 128 位 / 32 字符 | 低(已破解) | 极快 | 非安全场景、内部测试 |
| SHA-1 | 160 位 / 40 字符 | 中(已破解) | 快 | 不推荐新项目使用 |
| SHA-256 | 256 位 / 64 字符 | 高(未破解) | 中等 | 生产环境、数据去重 |
3.3 哈希碰撞与解决方案
哈希碰撞,就是两个不同的数据算出了相同的哈希值。理论上,只要输出长度有限,碰撞就必然存在。这叫「鸽巢原理」——256 位的输出只有 2^256 种可能,但输入是无限的。
实际中碰撞概率有多低?SHA-256 的碰撞概率大约是 2^-128。这个数字有多大?你每秒算 10 亿次,算上 100 亿年,碰撞概率还是微乎其微。
但「微乎其微」不等于「零」。在超大规模数据场景下,比如百亿级数据去重,碰撞还是有可能发生的。
解决方案一:双重哈希
用两种不同的哈希算法算两次。比如 SHA-256 + BLAKE2。两个哈希值都相同才判定为重复。碰撞概率直接降到两个算法碰撞概率的乘积。
// 双重哈希示例
String sha256 = DigestUtils.sha256Hex(data);
String blake2 = Blake2b.Digest.newInstance(256).digest(data);
// 只有两个哈希都匹配,才认为是重复数据
if (sha256.equals(existingSha256) && blake2.equals(existingBlake2)) {
// 重复数据
}
解决方案二:哈希 + 原始数据校验
哈希值相同后,再比对原始数据。这是最稳妥的方案。代价是多存一份原始数据或原始数据的部分特征。
我的做法:在去重系统里,我通常存「哈希值 + 数据长度 + 数据前 64 字节」。哈希碰撞后,先比对长度,再比对前 64 字节。99.99% 的假阳性在这一步就被过滤掉了。
解决方案三:使用更长的哈希
SHA-512 或者 SHA-3。输出越长,碰撞概率越低。代价是存储空间和计算时间增加。
3.4 哈希去重流程总览
下面这张图展示了哈希去重的完整流程。从原始数据到哈希计算,再到碰撞检测和去重决策。
流程其实不复杂。数据进来,算哈希,查索引。哈希不存在就直接入库。哈希存在了,走碰撞检测分支。检测通过才是真重复,否则就是碰撞,按新数据处理。