8.3 KiB
tags, create time
| tags | create time | ||||
|---|---|---|---|---|---|
|
2026-05-24 10:00 |
Redis Bloom Filter 布隆过滤器
概述
布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否存在于集合中。核心特性:
"Definitely not or probably yes" —— 说不存在则一定不存在;说存在则大概率存在(有极小误判概率)。
这个特性让它成为解决缓存穿透问题的利器。
[!question] 为什么需要布隆过滤器? 攻击者用大量不存在的 key 轰击系统,每个请求穿透缓存直打 DB,最终压垮数据库。布隆过滤器能在请求到达 DB 之前,用极低的内存成本拦截掉"肯定不存在"的请求。
一、核心原理
布隆过滤器的底层:一个 bit 数组 + 多个独立的哈希函数。
- bit 数组:长度为
m,初始全部为 0 - 哈希函数:
k个相互独立的哈希函数,每个函数将输入映射到[0, m-1]的某个位置
添加元素:对元素分别用 k 个哈希函数计算,将 bit 数组中对应位置全部置为 1。查询元素:用同样的 k 个哈希函数计算,所有对应位置都是 1 则判定"可能存在";任意一位是 0 则判定"一定不存在"。
[!tip] 为什么会有误判? 不同元素经过哈希计算后可能映射到相同位置(哈希冲突)。随着插入元素增多,查询一个不存在的元素时,恰好所有位置都被其他元素"碰巧"置为 1 的概率就会上升。
graph LR
A["element x"] --> H1["Hash1(x) = 3"]
A --> H2["Hash2(x) = 7"]
A --> H3["Hash3(x) = 11"]
H1 --> B1["bit[3] = 1"]
H2 --> B2["bit[7] = 1"]
H3 --> B3["bit[11] = 1"]
二、Redis 中的两种方案
方案 A:RedisBloom 模块(服务端)
RedisBloom 是 Redis 官方模块,通过 BF.ADD / BF.EXISTS 命令直接操作。
// go-redis 调用 RedisBloom
func checkWithRedisBloom(ctx context.Context, rdb *redis.Client, key, value string) (bool, error) {
result, err := rdb.Do(ctx, "BF.EXISTS", key, value).Int() // 1=可能存在, 0=一定不存在
if err != nil {
return false, err
}
return result == 1, nil
}
方案 B:Go 侧布隆过滤器库(客户端)
使用 github.com/bits-and-blooms/bloom/v3 在应用内存中维护过滤器。
import "github.com/bits-and-blooms/bloom/v3"
filter := bloom.NewWithEstimates(1_000_000, 0.0001) // 100万元素, 误判率0.01%
filter.AddString("user:10086") // 添加
exists := filter.TestString("user:10086") // 查询: true = 可能存在
三、缓存穿透防护实战
这是布隆过滤器最经典的应用场景。下面用一个完整的 Go 示例展示三层防护架构。
3.1 架构流程
graph TD
Req["客户端请求"] --> BF{"布隆过滤器判断"}
BF -- "不存在" --> Ret1["直接返回空, 拒绝请求"]
BF -- "可能存在" --> Cache{"查询 Redis 缓存"}
Cache -- "命中" --> Ret2["返回缓存数据"]
Cache -- "未命中" --> DB["查询 MySQL"]
DB -- "找到数据" --> SetCache["写入 Redis 缓存"]
SetCache --> Ret3["返回数据"]
DB -- "未找到" --> Ret4["返回空并缓存空值"]
3.2 完整代码示例
package main
import (
"context"
"encoding/json"
"time"
"github.com/bits-and-blooms/bloom/v3"
"github.com/redis/go-redis/v9"
)
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
type BloomCacheService struct { // 三层防护服务
rdb *redis.Client
filter *bloom.BloomFilter
}
// NewBloomCacheService 初始化:加载全量 ID 到布隆过滤器
func NewBloomCacheService(rdb *redis.Client) *BloomCacheService {
filter := bloom.NewWithEstimates(1_000_000, 0.0001) // 100万用户, 误判率0.01%
// 从 Redis Set 批量加载已有 ID(生产环境可从 DB 加载)
ctx := context.Background()
ids, _ := rdb.SMembers(ctx, "user:all_ids").Result()
for _, id := range ids {
filter.AddString(id)
}
return &BloomCacheService{rdb: rdb, filter: filter}
}
// GetUser 三层查询:Bloom -> Redis -> MySQL
func (s *BloomCacheService) GetUser(ctx context.Context, userID string) (*User, error) {
cacheKey := "user:" + userID
// 第一层:布隆过滤器拦截
if !s.filter.TestString(userID) {
return nil, nil // 一定不存在
}
// 第二层:Redis 缓存
data, err := s.rdb.Get(ctx, cacheKey).Bytes()
if err == nil {
if string(data) == "" {
return nil, nil // 缓存的空值
}
var user User
json.Unmarshal(data, &user)
return &user, nil
}
// 第三层:查询数据库
user, err := queryMySQL(userID)
if err != nil {
return nil, err
}
if user == nil {
s.rdb.Set(ctx, cacheKey, "", 5*time.Minute) // 缓存空值, 短 TTL
return nil, nil
}
bytes, _ := json.Marshal(user)
s.rdb.Set(ctx, cacheKey, bytes, 30*time.Minute)
return user, nil
}
// queryMySQL 模拟数据库查询
func queryMySQL(userID string) (*User, error) {
return nil, nil // 实际项目中查询 MySQL
}
[!question] 为什么还需要缓存空值? 布隆过滤器拦截了"肯定不存在"的请求,但如果某个 ID 曾经存在过(布隆过滤器会说"可能存在"),但实际已从 DB 删除,此时仍会穿透到 DB。缓存空值是第二道防线。
四、参数计算
选择布隆过滤器的两个关键参数:
| 参数 | 含义 | 影响 |
|---|---|---|
m |
bit 数组长度 | 越大误判率越低,但内存占用越多 |
k |
哈希函数个数 | 太少冲突多,太多计算慢 |
公式(基于期望元素数 n 和目标误判率 p):
m = -\frac{n \ln p}{(\ln 2)^2}, \quad k = \frac{m}{n} \ln 2
[!tip] 快速估算 对于 n = 100 万、p = 0.01% 的常见场景:
- m ≈ 19,170,117 bits ≈ 2.3 MB
- k ≈ 13 个哈希函数
相比之下,用 Redis Set 存 100 万个字符串 key 至少需要 几十 MB。布隆过滤器的内存优势是数量级的。
import "math"
func calcBloomParams(n int, p float64) (m int, k int) {
// m = -n * ln(p) / (ln2)^2
ln2 := math.Ln2
m = int(-float64(n) * math.Log(p) / (ln2 * ln2))
// k = (m/n) * ln2
k = int(math.Round(float64(m) / float64(n) * ln2))
return m, k
}
五、RedisBloom vs Go-side 对比
| 维度 | RedisBloom 模块 | Go-side 布隆库 |
|---|---|---|
| 部署 | 需安装 RedisBloom 模块 | 只需引入 Go 依赖 |
| 网络开销 | 每次查询一次网络往返 | 本地内存, 零网络开销 |
| 分布式一致性 | 天然共享 | 需自行同步 |
| 性能 | 受网络 RT 限制, < 10 万 QPS | 纯内存, 百万 QPS |
| 持久化 | 随 Redis RDB/AOF 自动持久化 | 需额外持久化机制 |
| 适用场景 | 多实例共享, 数据量大 | 单服务高频查询 |
[!tip] 如何选择?
- 微服务多实例:优先 RedisBloom,天然共享无需同步
- 单体服务、极致性能:Go-side 布隆库,避免网络开销
- 折中方案:Go-side 为主,定期从 Redis 同步 bit 数组快照
六、局限性
6.1 不支持删除
标准布隆过滤器不支持删除——将某位从 1 置为 0 可能影响其他元素判断。解决方案是计数布隆过滤器(Counting Bloom Filter),用计数器替代 1 bit,删除时计数器减 1。RedisBloom 的 Cuckoo Filter(CF.ADD / CF.EXISTS)也原生支持删除。
6.2 需要预加载
布隆过滤器必须在使用前加载全部合法元素。新增数据时需同步更新过滤器,初始化时需遍历全量数据。
[!question] 如果新增了数据但忘记更新布隆过滤器会怎样? 新增的 key 在布隆过滤器中查询会返回"不存在",导致合法请求被误拦截。这比缓存穿透更严重——用户明明有数据却查不到。所以务必确保新增数据时同步
BF.ADD或filter.AddString()。
6.3 误判率随使用上升
实际元素数量远超预估值 n 时,误判率会显著上升。建议预留 20%-30% 余量,监控实际元素数量,必要时重建过滤器。