目录
- [布隆过滤器实战(基于 go-redis 位图)](#布隆过滤器实战(基于 go-redis 位图))
-
- 核心概念
- [参数速查(1% 误判)](#参数速查(1% 误判))
1970年由Burton Howard Bloom提出,旨在解决哈希表空间效率低的问题。当时计算机内存极其昂贵,需要一种概率型数据结构来高效判断元素是否存在于集合中,而无需存储完整数据。
布隆过滤器实战(基于 go-redis 位图)
- 核心作用:高效判断"元素一定不存在"或"可能存在" - 用极小的空间代价换取时间效率,特别适合读多写少的场景。
- 简单来说,布隆过滤器原理就是把一个key进行几次哈希,转化为一组
bit,存储到一个bitmap里面,查询的时候,如果发现这个key转化出来的bit串的1在bitmap里面相应位置不都是1,说明这个key肯定没出现过,当然这里对于都是1的情况,可能存在误判(哈希冲突),比如本来key没出现过,但是计算完之后,发现在bitmap里面存在过,这时候流量就会打到数据库里面了,不过这种误判概率比较低,所以能够拦住大部分流量
核心概念
- 位图长度 m:Redis 中用 1 个 bit 表示一个"桶",m 决定桶的总数;桶越多,冲突越少,误判率越低,但内存线性增加。
- 哈希次数 k:每个元素进来后,用 k 个独立哈希函数算出 k 个桶位置,并把这些位设为 1;查询时同样检查这 k 位是否全为 1。
- 误判率 p :当 k、m 固定后,可推算理论误判率;经验公式 p ≈ ( 1 − e − k n m ) k p ≈ (1 - e^{\frac{-kn}{m}})^k p≈(1−em−kn)k,n 为预期元素总量。
参数速查(1% 误判)
| 预估元素 n | 位图长度 m | 哈希次数 k | 内存 |
|---|---|---|---|
| 100 万 | 1,140,000 | 7 | ≈ 137 KiB |
| 1 000 万 | 11,400,000 | 7 | ≈ 1.34 MiB |
| 1 亿 | 114,000,000 | 7 | ≈ 13.4 MiB |
代码示例如下
go
type BloomFilter struct {
rdb *redis.Client
key string
m uint32 // 位图长度
k int // 哈希长度
}
// 添加元素
func (b *BloomFilter) Add(ctx context.Context, value string) error {
pos := b.hashPositions(value)
pipe := b.rdb.Pipeline()
for _, p := range pos {
pipe.SetBit(ctx, b.key, int64(p), 1)
}
_, err := pipe.Exec(ctx)
return err
}
// 可能存在
func (b *BloomFilter) Exists(ctx context.Context, value string) (bool, error) {
pos := b.hashPositions(value)
pipe := b.rdb.Pipeline()
cmds := make([]*redis.IntCmd, len(pos))
for i, p := range pos {
cmds[i] = pipe.GetBit(ctx, b.key, int64(p))
}
if _, err := pipe.Exec(ctx); err != nil {
return false, err
}
for _, c := range cmds {
if c.Val() == 0 {
return false, nil
}
}
return true, nil
}
// 双重哈希生成K个位置
func (b *BloomFilter) hashPositions(value string) []uint32 {
// 第一次哈希:决定起始桶
h1 := fnv.New32a()
h1.Write([]byte(value))
base := h1.Sum32()
// 第二次哈希:决定步长,保证 k 次位置独立且均匀
h2 := sha1.Sum([]byte(value))
step := binary.BigEndian.Uint32(h2[0:4])
pos := make([]uint32, b.k) // 切片长度 = k, 体现"哈希次数"
for i := 0; i < b.k; i++ {
// 第i次位置 = (base + i * step) mod m
pos[i] = (base + uint32(i)*step) % b.m
}
return pos
}
func TestBloom(t *testing.T) {
filter := BloomFilter{
rdb: redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
}),
key: "bloom:uids",
m: 1140000,
k: 7,
}
key := "user:9527"
err := filter.Add(t.Context(), key)
if err != nil {
t.Fatal(err)
}
exists, err := filter.Exists(t.Context(), key)
if err != nil {
t.Fatal(err)
}
t.Log("1", exists)
if exists, err = filter.Exists(t.Context(), "user:004"); err != nil {
t.Fatal(err)
}
t.Log("2", exists)
}
// 输出
// 1 true
// 2 false