Files
cs-note/hhs/Redis/14-BloomFilter.md
T

285 lines
10 KiB
Markdown
Raw Normal View History

2026-05-24 21:18:14 +08:00
---
tags: [Redis, 缓存, 数据结构, BloomFilter]
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 的概率就会上升。
```mermaid
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 模块(服务端)
2026-05-25 23:50:33 +08:00
RedisBloom 是 Redis 官方模块,**默认不随 Redis 一起安装**,需要单独编译或通过 Docker 加载:
```bash
# Docker 方式(推荐)
docker run -p 6379:6379 redis/redis-stack-server # 内置 RedisBloom
```
核心命令:
| 命令 | 说明 |
|------|------|
| `BF.RESERVE key error_rate capacity` | 创建自定义布隆过滤器(生产环境推荐) |
| `BF.ADD key item` | 添加元素(自动创建默认参数的过滤器) |
| `BF.EXISTS key item` | 查询元素,返回 1=可能存在 / 0=一定不存在 |
| `BF.MADD key item1 item2 ...` | 批量添加 |
| `BF.MEXISTS key item1 item2 ...` | 批量查询 |
> [!tip] 生产环境务必用 `BF.RESERVE` 预创建
> `BF.ADD` 首次调用时会以默认参数自动创建过滤器(容量约 100,误判率 0.01),数据量稍大就会急剧膨胀误判率。正确做法是先 `BF.RESERVE user_filter 0.0001 1000000` 显式声明参数。
2026-05-24 21:18:14 +08:00
```go
// 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` 在应用内存中维护过滤器。
```go
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 架构流程
```mermaid
graph TD
Req["客户端请求"] --> BF{"布隆过滤器判断"}
BF -- "不存在" --> Ret1["直接返回空, 拒绝请求"]
BF -- "可能存在" --> Cache{"查询 Redis 缓存"}
Cache -- "命中" --> Ret2["返回缓存数据"]
Cache -- "未命中" --> DB["查询 MySQL"]
DB -- "找到数据" --> SetCache["写入 Redis 缓存"]
SetCache --> Ret3["返回数据"]
DB -- "未找到" --> Ret4["返回空并缓存空值"]
```
### 3.2 完整代码示例
```go
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
2026-05-25 23:50:33 +08:00
if err := json.Unmarshal(data, &user); err != nil {
return nil, err // JSON 反序列化失败
}
2026-05-24 21:18:14 +08:00
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**。布隆过滤器的内存优势是数量级的。
```go
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 不支持删除
2026-05-25 23:50:33 +08:00
标准布隆过滤器不支持删除——将某位从 1 置为 0 可能影响其他元素判断。两种解决方案:
| 方案 | 原理 | 优点 | 缺点 |
|------|------|------|------|
| **计数布隆过滤器** | 用计数器替代 1 bit,删除时减 1 | 兼容标准布隆过滤器思路 | 内存开销增大 3-4 倍 |
| **Cuckoo Filter** | 基于 cuckoo hashing,直接存储元素指纹 | 支持删除、查询更快、空间更优 | 容量接近满时插入可能失败 |
RedisBloom 中 Cuckoo Filter 的命令:
```bash
CF.RESERVE key capacity # 创建
CF.ADD key item # 添加
CF.EXISTS key item # 查询
CF.DEL key item # 删除(布隆过滤器做不到)
```
> [!tip] 何时用 Cuckoo Filter?
> 如果你的场景**需要删除过期元素**(比如用户注销后移除 ID),优先用 Cuckoo Filter。如果只做"只增不查删"的穿透防护,标准布隆过滤器更简单高效。
2026-05-24 21:18:14 +08:00
### 6.2 需要预加载
布隆过滤器必须在使用前加载全部合法元素。新增数据时需同步更新过滤器,初始化时需遍历全量数据。
> [!question] 如果新增了数据但忘记更新布隆过滤器会怎样?
> 新增的 key 在布隆过滤器中查询会返回"不存在",导致合法请求被误拦截。这比缓存穿透更严重——用户明明有数据却查不到。所以务必确保新增数据时同步 `BF.ADD` 或 `filter.AddString()`。
2026-05-25 23:50:33 +08:00
> [!warning] Go-side 过滤器的重启问题
> Go 内存中的布隆过滤器在进程重启后**完全丢失**。两种应对策略:
> 1. **启动时重建**:从 DB 全量加载 ID 重新构建(数据量大时启动慢,可用 goroutine 并行加载)
> 2. **持久化 bit 数组**:将 bit 数组序列化到 Redis 或文件,启动时反序列化恢复(快但需要额外维护一致性)
>
> 如果重建成本不可接受,优先考虑 RedisBloom——数据随 Redis 持久化,应用重启无感知。
2026-05-24 21:18:14 +08:00
### 6.3 误判率随使用上升
实际元素数量远超预估值 `n` 时,误判率会显著上升。建议预留 20%-30% 余量,监控实际元素数量,必要时重建过滤器。
## 关联笔记
- [[hhs/Redis/10-缓存架构模式]]
- [[hhs/Redis/02-核心数据类型]]
- [[hhs/Redis/README]]