利用 Redis 布隆(布谷鸟)过滤器轻松对千万数据量进行判重
Todd

1.问题背景

在我们的生产追溯系统中,一个棘手的问题困扰了我们很长时间:如何高效地对大量SN(序列号)进行查重?我们的TDengine数据库中存储了超过1000万条SN记录,且每天还在持续增长。直接查询TDengine的性能很差,因为你不确定时间范围,所以全库扫描最差情况下一次查询需要8秒左右,特别是在30台机器并发查询的情况下,系统几乎无法正常工作。
随着业务规模扩大,预计很快数据量会增长到1亿条,使用传统数据库索引方式已经难以满足高并发、低延迟的需求。我们需要一个更高效的解决方案。

2. 解决方案探索

2.1 传统方案的局限性

最初我们考虑了几种常见的解决方案:

  • 数据库索引优化:虽然索引可以提升查询速度,但在上亿数据量级下效果有限
  • 分库分表:实现复杂,且仍然无法从根本上解决高并发查询问题
  • 内存缓存:全量缓存10-100M的SN数据需要大量内存,成本过高

2.2 布隆过滤器

2.2.1 概率数据结构的引入

在深入研究后,我们发现概率数据结构可能是解决这个问题的关键。这类数据结构使用极少的内存就能处理大规模数据集,代价是引入可控的误判率。
我们主要考察了两种方案:

  • Bloom过滤器:空间效率高,但不支持删除操作
  • 布谷鸟过滤器(Cuckoo Filter):支持删除操作,且误判率和空间效率可控
    因为存在 SN 撤销的情况,所以最终我们选择了布谷鸟过滤器作为核心技术方案。

2.2.2 布谷鸟过滤器

布谷鸟过滤器是一种基于哈希表的概率数据结构,它支持插入、删除和查询操作。

3. 布谷鸟过滤器原理

3.1 基本原理

布谷鸟过滤器是一种概率数据结构,它通过哈希函数将元素映射到固定大小的表中,具有以下特点:

  • 空间效率:存储1000万个元素只需约15MB内存
  • 查询性能:查询时间复杂度O(1),实际性能<1μs
  • 支持删除:与Bloom过滤器不同,可以删除已添加的元素
  • 可控误判:可以通过调整参数控制误判率

3.2 误判处理

布谷鸟过滤器最关键的一点是它可能出现”假阳性”(误报),即报告存在实际上不存在的元素。对此,我们设计了二次确认机制:

  • 首先在布谷鸟过滤器中快速查询
  • 当过滤器报告元素存在时,再去TDengine中进行确认
  • 如发现是误判,自动修正过滤器
    这种策略充分发挥了布谷鸟过滤器的速度优势,同时保证了结果的准确性。

4. 实现架构

4.1 技术栈选择

  • Redis + RedisBloom模块:提供布谷鸟过滤器的实现
  • Laravel框架:构建Web管理界面和API服务
  • TDengine:作为数据源和二次确认的数据库

4.2 系统架构

我们的SN查重服务由以下几部分组成:

  • 数据层:TDengine存储原始SN数据
  • 缓存层:Redis存储布谷鸟过滤器
  • 服务层:Laravel API提供查重和管理功能
  • 管理层:Dcat Admin实现可视化管理
  • 客户端:设备端通过API进行SN查询

5 核心功能实现和踩坑

5.1 使用RedisBloom模块

RedisBloom模块是Redis的扩展模块,提供了布谷鸟过滤器的实现。但是我们生产环境都是 docker 部署,所以我们找了一个综合的镜像,包含了 RedisBloom 模块。就是 redis-stack-server 镜像 https://redis.io/docs/latest/operate/oss_and_stack/install/install-stack/docker/。

注意:redis-stack-server 镜像中低于 7.0 的版本是不支持布谷鸟过滤器的。

5.2 在 laravel 中使用布谷鸟过滤器

因内Laravel 的原生 redis 扩展不支持布谷鸟过滤器,所以要使用 rawCommand 来使用布谷鸟过滤器。

1
2
3
4
5
6
7
Redis::connection()->client()->rawCommand(
'CF.RESERVE',
$this->filterKey,
$capacity,
'EXPANSION', 2,
'MAX_ITERATIONS', 30
);

这是布谷鸟过滤器的初始化命令,$capacity 是过滤器的容量,$expansion 是扩展因子,$max_iterations 是最大迭代次数。

然后在初始化中,加载历史 SN到 Redis 中,

1
2
3
4
$result = taosHttpClient($sql);
foreach ($result['data'] as $item) {
Redis::connection()->client()->rawCommand('CF.ADDNX', $this->filterKey, $item['sn']);
}

这只是 demo,实际场景是要分批加载的,否则单次加载太多,内存会直接爆掉。

注意:CF.ADDNX 是布谷鸟过滤器的添加命令,如果元素已经存在,则不添加。而 CF.ADD 是布隆过滤器的添加命令,如果元素已经存在,依然会添加。 见: https://redis.io/docs/latest/commands/cf.add/

5.3 核心判重代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public function checkSN(Request $request)
{
$sn = $request->input('sn');

// 在布谷鸟过滤器中检查SN是否存在
$result = Redis::connection()->client()->rawCommand('CF.EXISTS', $this->filterKey, $sn);

// 如果SN在过滤器中存在,可能有冲突(可能有误判)
if ($result === 1) {
// 二次确认:去TDEngine中查询该SN是否真的存在
$isRealConflict = $this->confirmSNInTDEngine($sn);

if ($isRealConflict) {
// SN确实存在,返回冲突信息
return responseJson([], 500, [], 'SN已存在,不能重复使用', 500);
} else {
// 误判情况,该SN实际不存在,自动修正
$this->addSNToFilter($sn);
return responseJson([], 200, [], 'SN可用(已自动修正误判)', 0);
}
} else {
// SN不存在,可以使用,但是需要加到过滤器中,再次请求的时候就又是使用过了。
$this->addSNToFilter($sn);
return responseJson([], 200, [], 'SN可用', 0);
}
}

6. 性能对比

6.1 查询性能 (当前 1000w 数据量下)

方案单次查询时间内存占用支持删除额外条件
直接查询TDengine~8秒TDEngine 服务器会产生大量 CPU 和内存占用
MySQL索引~500ms需要大量索引和分表,同时占用大量硬盘
Redis集合~1ms高(~500MB)无法满足日后数据量增长
布谷鸟过滤器<1μs低(~15MB)完美

7. 总结

布谷鸟过滤器在当前 1000w 数据量下,查询性能和内存占用都非常优秀,支持删除操作,且误判率和空间效率可控。

布谷鸟过滤器为我们的SN查重服务带来了质的飞跃,将http查询时间降至秒级,同时大幅降低了资源消耗。这种技术方案特别适合大规模数据的快速查重场景。

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 84.7k 访客数 访问量