3. 冲突检测算法:哈希检测法、布隆过滤器、基于数据库的检测、基于缓存的检测
数据唯一性冲突检测,说白了就是回答一个问题:“这条数据,我是不是已经见过了?”
听起来简单,但在海量数据、高并发、分布式环境下,这个问题的答案可没那么好拿。我这些年踩过的坑,十有八九都跟“重复数据”有关。有的重复数据悄无声息地进了库,等到对账的时候才发现,那叫一个头疼。
今天咱们就聊聊四种主流的检测算法。每种都有它的脾气,选对了事半功倍,选错了……嗯,你会后悔的。
核心观点:没有银弹。哈希检测法最快但最占内存,布隆过滤器最省空间但有误判,数据库检测最可靠但最慢,缓存检测是折中方案。实际项目中,我通常组合使用两到三种。
3.1 哈希检测法:最快,但最“吃”内存
哈希检测法的思路很直接:把每条数据的唯一标识(比如订单号、身份证号)算出一个哈希值,存到内存里的哈希集合中。新数据来了,先算哈希,再查集合——在不在,一清二楚。
我个人习惯用 MurmurHash3 或者 xxHash,它们比 MD5 快得多,而且冲突率极低。你想想看,MD5 虽然安全,但用在检测重复上有点大材小用,性能还跟不上。
我的经验:在单机内存足够的情况下,哈希检测法是最简单粗暴的方案。我曾经在一个日处理 500 万条订单的系统里用过,延迟基本在微秒级。但要注意——内存会随着数据量线性增长。1 亿条订单的 ID,每个 64 位,光存哈希表就得 1.6GB 左右。
// 伪代码示例:哈希检测法
public class HashDeduplicator {
private Set<Long> hashSet = new HashSet<>();
public boolean isDuplicate(String data) {
long hash = murmurHash3(data); // 计算哈希
return !hashSet.add(hash); // 如果已存在,返回 true
}
}
注意:哈希冲突虽然概率极低,但理论上存在。如果你做的是金融交易、医疗数据这类零容忍场景,别只用哈希检测法。我建议哈希检测法只做“快速过滤”,最终确认交给数据库。
3.2 布隆过滤器:省内存的“概率选手”
布隆过滤器是个很有意思的东西。它用多个哈希函数把数据映射到一个位数组上。判断时,只要有一个位是 0,那数据一定不存在;如果所有位都是 1,那数据可能存在。
说白了,它是个“宁可错杀一千,绝不放过一个”的选手。误判率可以控制,一般在 1% 以下。
我记得在做一个爬虫去重系统时,URL 量级到了几十亿,用哈希表内存根本扛不住。换成布隆过滤器,内存只用了原来的 1/10,误判率控制在 0.1%。效果非常好。
| 特性 | 哈希检测法 | 布隆过滤器 |
|---|---|---|
| 内存占用 | 高(线性增长) | 低(固定位数组) |
| 准确性 | 精确(理论无误差) | 有误判(可控制) |
| 删除支持 | 支持 | 不支持(标准版) |
| 适用场景 | 小数据量、高精度 | 海量数据、容忍误判 |
避坑指南:布隆过滤器不支持删除元素。如果你需要“撤回”某条数据的检测结果,标准布隆过滤器做不到。我曾经踩过这个坑——后来改用 Counting Bloom Filter 才解决。但 Counting 版本内存会翻几倍,需要权衡。
// 伪代码示例:布隆过滤器
public class BloomFilterDetector {
private BitSet bits = new BitSet(1 << 24); // 16M 位
private int[] seeds = {3, 5, 7, 11, 13}; // 5个哈希函数
public boolean mightContain(String data) {
for (int seed : seeds) {
int hash = hash(data, seed);
if (!bits.get(hash)) {
return false; // 一定不存在
}
}
return true; // 可能存在(有误判)
}
public void add(String data) {
for (int seed : seeds) {
int hash = hash(data, seed);
bits.set(hash);
}
}
}
3.3 基于数据库的检测:最可靠,但最慢
数据库检测,就是利用数据库的唯一索引或者 SELECT COUNT(*) 来判断数据是否已存在。这是最传统、最可靠的方法。
但问题也很明显——每次检测都要走一次磁盘 I/O。在高并发场景下,数据库会成为瓶颈。我见过一个项目,每秒 1 万次写入,每次都要先查数据库判断是否重复,结果数据库连接池直接被打满。
为什么会这样?因为数据库的 INSERT ... ON DUPLICATE KEY UPDATE 虽然能处理重复,但底层还是走了索引扫描。并发高了,锁竞争就上来了。
我的建议:数据库检测适合做“最终裁决”,不适合做“第一道防线”。我通常的做法是:先用布隆过滤器或缓存挡掉 99% 的重复请求,剩下的 1% 交给数据库做精确判断。这样数据库的压力就小多了。
-- SQL 示例:利用唯一索引检测重复
CREATE TABLE orders (
order_id VARCHAR(64) PRIMARY KEY,
user_id INT,
amount DECIMAL(10,2),
created_at DATETIME
);
-- 插入时检测重复(MySQL 语法)
INSERT INTO orders (order_id, user_id, amount, created_at)
VALUES ('ORD2024001', 1001, 299.00, NOW())
ON DUPLICATE KEY UPDATE
user_id = VALUES(user_id); -- 如果重复,更新其他字段
3.4 基于缓存的检测:折中的“快车道”
缓存检测,就是把最近处理过的数据标识存到 Redis、Memcached 这类缓存里。新数据来了,先查缓存。缓存里有,说明重复;缓存里没有,再去查数据库。
这其实是个“时间换空间”的折中方案。缓存比数据库快得多(毫秒级 vs 几十毫秒),但比内存哈希表慢一点。好处是缓存可以持久化,重启不丢数据。
我记得在做一个秒杀系统时,就用 Redis 的 SETNX 命令来做唯一性检测。用户每次下单,先 SETNX order_id 1,如果返回 1 说明没重复,继续处理;返回 0 说明重复,直接拒绝。这个方案在每秒 5000 次请求下依然稳定。
注意:缓存有丢失风险。如果 Redis 宕机,缓存里的数据就丢了。重启后,所有请求都会被认为是“新数据”,导致重复写入。解决方案:缓存 + 数据库双重检测,或者给缓存做持久化(RDB/AOF)。
// 伪代码示例:基于 Redis 的缓存检测
public class RedisDeduplicator {
private Jedis jedis;
public boolean isDuplicate(String key) {
// SETNX:如果 key 不存在则设置,返回 1;存在则返回 0
Long result = jedis.setnx("dedup:" + key, "1");
if (result == 1) {
// 设置过期时间,防止缓存无限增长
jedis.expire("dedup:" + key, 3600); // 1小时过期
return false; // 不是重复
}
return true; // 是重复
}
}
3.5 如何选择?一张表说清楚
你可能会问:这么多方法,我到底该用哪个?
我的回答是:看你的数据量、精度要求和硬件资源。下面这张表是我自己总结的,你可以直接拿去用。
| 场景 | 推荐算法 | 理由 |
|---|---|---|
| 数据量 < 1000 万,单机 | 哈希检测法 | 简单、快速、精确 |
| 数据量 > 1 亿,容忍误判 | 布隆过滤器 | 省内存,性能好 |
| 金融、医疗等零容忍场景 | 数据库检测 | 最可靠,支持事务 |
| 高并发、低延迟 | 缓存检测 | 读写快,可扩展 |
| 海量数据 + 高精度 | 布隆过滤器 + 数据库 | 兼顾性能与可靠性 |
我的最终建议:不要只依赖一种算法。我现在的项目里,都是“组合拳”——布隆过滤器做第一道快速过滤,Redis 缓存做第二道缓冲,数据库做最终裁决。三层下来,既保证了性能,又保证了数据唯一性。