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 缓存做第二道缓冲,数据库做最终裁决。三层下来,既保证了性能,又保证了数据唯一性。