欢迎光临
我们一直在努力

Redis 布隆过滤器——从原理到 Spring Boot 整合,防重复消费一把梭

Redis 布隆过滤器——从原理到 Spring Boot 整合,防重复消费一把梭

用户是否已经点赞过这篇笔记?直接查 Redis Set——一个 key 存几千个 noteId,一百万用户点赞就是一百万条记录,几 GB 内存。布隆过滤器用几百 KB 就能存上百万条"是否存在"的记录,判断一条不存在一定准确,判断一条存在可能误判——这个特性恰好就是消息去重、缓存穿透保护的最优解。这篇文章从布隆过滤器的位图原理讲起,到 Docker 安装 RedisBloom 模块、基本命令、Spring Boot + Lua 脚本整合,用真实代码串下来。


布隆过滤器是什么——先忘掉 Redis,看一张位图

布隆过滤器的底层就是一张很长的二进制位数组(BitArray),初始全是 0。

#mermaid-svg-0S77YMDf1TMtIdQ0{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0S77YMDf1TMtIdQ0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0S77YMDf1TMtIdQ0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0S77YMDf1TMtIdQ0 .error-icon{fill:#552222;}#mermaid-svg-0S77YMDf1TMtIdQ0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0S77YMDf1TMtIdQ0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0S77YMDf1TMtIdQ0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0S77YMDf1TMtIdQ0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0S77YMDf1TMtIdQ0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0S77YMDf1TMtIdQ0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0S77YMDf1TMtIdQ0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0S77YMDf1TMtIdQ0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0S77YMDf1TMtIdQ0 .marker.cross{stroke:#333333;}#mermaid-svg-0S77YMDf1TMtIdQ0 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0S77YMDf1TMtIdQ0 p{margin:0;}#mermaid-svg-0S77YMDf1TMtIdQ0 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-0S77YMDf1TMtIdQ0 .cluster-label text{fill:#333;}#mermaid-svg-0S77YMDf1TMtIdQ0 .cluster-label span{color:#333;}#mermaid-svg-0S77YMDf1TMtIdQ0 .cluster-label span p{background-color:transparent;}#mermaid-svg-0S77YMDf1TMtIdQ0 .label text,#mermaid-svg-0S77YMDf1TMtIdQ0 span{fill:#333;color:#333;}#mermaid-svg-0S77YMDf1TMtIdQ0 .node rect,#mermaid-svg-0S77YMDf1TMtIdQ0 .node circle,#mermaid-svg-0S77YMDf1TMtIdQ0 .node ellipse,#mermaid-svg-0S77YMDf1TMtIdQ0 .node polygon,#mermaid-svg-0S77YMDf1TMtIdQ0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0S77YMDf1TMtIdQ0 .rough-node .label text,#mermaid-svg-0S77YMDf1TMtIdQ0 .node .label text,#mermaid-svg-0S77YMDf1TMtIdQ0 .image-shape .label,#mermaid-svg-0S77YMDf1TMtIdQ0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-0S77YMDf1TMtIdQ0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0S77YMDf1TMtIdQ0 .rough-node .label,#mermaid-svg-0S77YMDf1TMtIdQ0 .node .label,#mermaid-svg-0S77YMDf1TMtIdQ0 .image-shape .label,#mermaid-svg-0S77YMDf1TMtIdQ0 .icon-shape .label{text-align:center;}#mermaid-svg-0S77YMDf1TMtIdQ0 .node.clickable{cursor:pointer;}#mermaid-svg-0S77YMDf1TMtIdQ0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0S77YMDf1TMtIdQ0 .arrowheadPath{fill:#333333;}#mermaid-svg-0S77YMDf1TMtIdQ0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0S77YMDf1TMtIdQ0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0S77YMDf1TMtIdQ0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0S77YMDf1TMtIdQ0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0S77YMDf1TMtIdQ0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0S77YMDf1TMtIdQ0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0S77YMDf1TMtIdQ0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0S77YMDf1TMtIdQ0 .cluster text{fill:#333;}#mermaid-svg-0S77YMDf1TMtIdQ0 .cluster span{color:#333;}#mermaid-svg-0S77YMDf1TMtIdQ0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0S77YMDf1TMtIdQ0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0S77YMDf1TMtIdQ0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-0S77YMDf1TMtIdQ0 .icon-shape,#mermaid-svg-0S77YMDf1TMtIdQ0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0S77YMDf1TMtIdQ0 .icon-shape p,#mermaid-svg-0S77YMDf1TMtIdQ0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0S77YMDf1TMtIdQ0 .icon-shape .label rect,#mermaid-svg-0S77YMDf1TMtIdQ0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0S77YMDf1TMtIdQ0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0S77YMDf1TMtIdQ0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0S77YMDf1TMtIdQ0 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

位数组(初始全 0)

0

0

0

0

0

0

0

0

往里面添加一个元素时,布隆过滤器用 N 个哈希函数算出 N 个位置,把这 N 个位置都置为 1。

添加 noteId=888:
hash1("888") → 位置 2
hash2("888") → 位置 5
hash3("888") → 位置 7

位数组变为:
[0, 0, 1, 0, 0, 1, 0, 1, …]
↑ ↑ ↑

添加 noteId=666:
hash1("666") → 位置 1
hash2("666") → 位置 5
hash3("666") → 位置 6

位数组变为:
[0, 1, 1, 0, 0, 1, 1, 1, …]
↑ ↑ ↑ ↑ ↑

判断一个元素是否存在: 用同样的 N 个哈希函数算出 N 个位置,看是不是全为 1。

  • 全为 1 → 可能存在(这几个位置可能是被其他元素置为 1 的,恰好重合了)
  • 有一个不是 1 → 一定不存在(如果添加过,这个位置肯定被置为 1 了)

核心结论:说不存在一定准确,说存在可能是误判。 误判率由位数组长度和哈希函数数量决定,RedisBloom 默认误判率 1%。


为什么用它——跟 Set 比内存,跟 DB 比速度

场景:判断用户是否已经点赞过这篇笔记。

方案100 万条记录占多少内存查一次多快
MySQL 查表 几十 MB + 索引 数 ms
Redis Set SISMEMBER,几十 MB 不到 1ms
布隆过滤器 BF.EXISTS,几百 KB 不到 1ms

布隆过滤器的空间效率接近极致——一个 key 只用 1.2 MB 就能存 100 万条记录(误判率 1%)。对消息去重、缓存穿透保护、黑名单过滤这些"能接受少量误判"的场景,再合适不过。

代价就是那 1% 的误判。 说不存在一定不存在,说存在有小概率其实不存在。对点赞去重来说——最多漏掉一个"用户其实没点赞,但布隆说点过了"的情况,不会重复;对缓存穿透来说——布隆说过滤掉的 key 一定不在 DB 里,绝对不会误杀。


Docker 安装——三种方案

方案一:还没装 Redis,直接拉带模块的镜像

redis/redis-stack-server 镜像内置了 RedisBloom、RedisJSON、RediSearch 等常用模块,最省事:

docker run -d \\
–name redis \\
-p 6379:6379 \\
redis/redis-stack-server:latest

启动后 BF.ADD 就能直接用。

方案二:已有 Redis 容器,换镜像重启

数据在 volume 里不会丢,换个带模块的镜像就行:

docker stop redis && docker rm redis

docker run -d \\
–name redis \\
-p 6379:6379 \\
-v redis-data:/data \\
redis/redis-stack-server:latest

方案三:不想换镜像,单独挂载 RedisBloom 模块文件

RedisBloom | Github

如果不想换镜像——比如用的是官方 redis:7.2,已经有很多配置——可以只把 RedisBloom 的 .so 文件挂进去,然后在 redis.conf 里加载。

# 1. 下载编译好的 redisbloom.so
# 从 https://github.com/RedisBloom/RedisBloom/releases 下载对应版本的 .so 文件
# 或者直接 wget:
wget https://github.com/RedisBloom/RedisBloom/releases/download/v2.8.5/redisbloom.Linux-x86_64-v2.8.5.zip
unzip redisbloom.Linux-x86_64-v2.8.5.zip

# 2. 修改 redis.conf,加一行:
# loadmodule /usr/local/etc/redis/redisbloom.so
echo 'loadmodule /usr/local/etc/redis/redisbloom.so' >> ./redis.conf

# 3. 把模块文件和 redis.conf 一起挂进容器
docker run -d \\
–name redis \\
-p 6379:6379 \\
-v ./redisbloom.so:/usr/local/etc/redis/redisbloom.so \\
-v ./redis.conf:/usr/local/etc/redis/redis.conf \\
redis:7.2 redis-server /usr/local/etc/redis/redis.conf

三项配齐:redis.conf 里配了 loadmodule、.so 文件挂进去了、容器启动时指定了自定义配置文件。

从 Redis8 开始就内置在 Redis 了,就不用单独配置了。


基本命令

连进去:

docker exec -it redis-bloom redis-cli

创建一个布隆过滤器

# BF.RESERVE key error_rate capacity
# 误判率 0.01(1%),预计存 100000 条
BF.RESERVE user:like:10001 0.01 100000

如果不调用 RESERVE 直接 ADD,RedisBloom 会帮你自动创建一个默认参数的过滤器。

添加和查询

# 添加一个元素——返回 1(新元素)或 0(已存在)
BF.ADD user:like:10001 888
# (integer) 1

# 再添加同样的——返回 0,说明已存在
BF.ADD user:like:10001 888
# (integer) 0

# 判断是否存在——返回 1(可能存在)或 0(一定不存在)
BF.EXISTS user:like:10001 888
# (integer) 1

BF.EXISTS user:like:10001 999
# (integer) 0

批量操作

# 批量添加
BF.MADD user:like:10001 111 222 333
# 1) (integer) 1
# 2) (integer) 1
# 3) (integer) 1

# 批量判断
BF.MEXISTS user:like:10001 111 888 999
# 1) (integer) 1
# 2) (integer) 1
# 3) (integer) 0

查看信息

BF.INFO user:like:10001
# Size: 131072 ← 位数组大小
# Number of items inserted: 5

布隆过滤器不能删除

BF.ADD 之后没有 BF.DEL。因为一个位置可能被多个元素共用——删掉会误伤别的元素。RedisBloom 从 2.4 版本开始支持 BF.INSERT 做一些删除操作,但复杂度高,日常不推荐。

如果需要能删除的过滤器,用 CF.ADD / CF.DEL——Cuckoo Filter,这是另一个话题了。


Spring Boot 整合——用 Lua 脚本操作布隆过滤器

Spring Data Redis 没有直接提供 BFOps 方法,操作布隆过滤器要借助 Lua 脚本。把 BF.EXISTS 和 BF.ADD 的逻辑写进 .lua 文件,Java 端用 DefaultRedisScript 加载执行。

场景:判断用户是否已经点赞过某篇笔记

— resources/lua/bloom_note_like_check.lua
local key = KEYS[1] — bloom:user:like:{userId}
local noteId = ARGV[1] — 笔记 ID

— 布隆过滤器不存在 → 返回 -1
if redis.call('EXISTS', key) == 0 then
return 1
end

— 判断是否已点赞
local isLiked = redis.call('BF.EXISTS', key, noteId)
if isLiked == 1 then
return 1 — 已经点赞了
end

— 未点赞,添加进去
redis.call('BF.ADD', key, noteId)
return 0 — 点赞成功(之前没点过)

// Java 端执行
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptSource(new ResourceScriptSource(
new ClassPathResource("/lua/bloom_note_like_check.lua")));
script.setResultType(Long.class);

String bloomKey = RedisKeyConstants.buildBloomUserNoteLikeListKey(userId);
Long result = redisTemplate.execute(script,
Collections.singletonList(bloomKey), noteId);

// 返回值:1=已点赞,0=点赞成功,-1=布隆过滤器不存在

三种返回值对应三种情况,定义枚举:

@Getter
@AllArgsConstructor
public enum BloomCheckResultEnum {
ALREADY_EXISTS(1, "已存在"),
NOT_EXIST(0, "不存在,已添加"),
FILTER_NOT_EXIST(1, "布隆过滤器不存在");
}

场景:消息去重——防止重复消费

— 先建布隆过滤器(如果不存在),再判断
local key = KEYS[1] — bloom:user:note:publish:20260710
local userId = ARGV[1] — 发布者 ID

if redis.call('EXISTS', key) == 0 then
redis.call('BF.ADD', key, '')
redis.call('EXPIRE', key, 20 * 60 * 60) — 当天有效
end

return redis.call('BF.EXISTS', key, userId)
— 返回 1:已处理过 → 跳过
— 返回 0:没处理过 → 可以处理

Java 端配合落库使用:布隆返回 0 → 落库 → 再调一次 BF.ADD 标记为已处理。万一布隆误判了(说已存在但其实不存在),最坏情况就是少处理一条——不会出现重复落库。


布隆过滤器的两个"不能"

1. 不能删除元素。 一个 bit 可能被多个元素共用,删掉就误伤了。要支持删除用 Cuckoo Filter。

2. 不能遍历所有元素。 布隆过滤器的底层是位数组,没有存元素本身,只存了"存在与否"的痕迹。你想列出所有被添加过的 noteId——做不到。它只回答"这个 ID 存不存在",不回答"存过哪些 ID"。


什么时候用、什么时候不用

该用:

  • 防缓存穿透——请求过来先过布隆,布隆说不存在的 key 一定不在 DB 里,直接返回空,不用查 DB
  • 消息去重——MQ 重复消费时,布隆判断是否处理过
  • 黑名单——判断用户/ IP 是否在名单里
  • 点赞/关注去重——判断是否已经操作过

不该用:

  • 不能接受误判——比如支付回调去重,漏一笔就是钱的问题
  • 需要删除元素——布隆删不了
  • 需要列出所有元素——布隆不存原始数据
  • 需要精确判断——直接用 Set
赞(0)
未经允许不得转载:171主机测评 » Redis 布隆过滤器——从原理到 Spring Boot 整合,防重复消费一把梭
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址