问题背景:为什么我们需要“模糊记忆”?
从处理数百万交易的银行API,到通过协调自动化生产线的工业平台管理重要患者数据的医院系统。
这些系统有一个共同的特点:它们必须每天处理数百万个API调用,同时保持出色的性能和可预测的响应时间。
在这种情况下,即使看似简单的操作在扩大业务量时也可能成为关键瓶颈。
一个完美的例子是,在为已经达到1亿用户的社交网络开发用户注册API时,您可以找到什么。
每次有人尝试注册时,您都需要检查电子邮件是否已经存在于数据库中。
看似无辜的咨询SELECT COUNT(*) FROM users WHERE email = ?可能看起来微不足道,但是当在1亿行的表中每分钟执行数千次时,即使使用最好的索引,数据库也开始受到影响。
矛盾的是,这些查询中的绝大多数返回零结果——大多数被验证的电子邮件实际上都是新的。
你实际上是在浪费计算资源,用一种更有效的方法提前确认你可以知道的东西。
这种情况在许多上下文中重复出现:在Web爬虫中已经访问的URL的验证,分布式缓存中的关键字验证,反垃圾邮件过滤器,重复数据删除系统。
检查元素是否“可以存在”的所有成本都远低于最终检查的成本。
原理概述
Bloom Filter由一组位(最初全部为零)和一组哈希函数组成。当你想“记住”一个元素时:
计算元素的几个哈希函数
将这些值用作位数组中的索引
将所有匹配位设置为 1
检查某个元素是否存在:
计算相同的哈希函数
检查所有匹配位是否在 1
如果连一个都在0中,则元素绝对不存在
如果每个人都在1,元素可能就在那里。
美丽且紧凑的:无论你插入多少元素,Bloom过滤器的大小保持不变。
对于我们的1亿封电子邮件,而不是千兆字节的内存,我们只能使用几兆字节,同时保持1%的假阳性概率。
- 用 []uint32 充当位图,节省内存。
- 双重哈希 + 线性组合模拟 k 个独立哈希函数。
以下是Golang实现代码:
package utils
import (
"errors"
"hash/fnv"
"sync"
)
type TBloomFilter struct {
bitArray []uint32 // 位
bitCount uint32 // 总位数
hashFunctions int // 哈希函数个数
mu sync.RWMutex // 读写锁
}
// bitCount:总位数;hashFunctions:哈希函数个数(k)
func NewBloomFilter(bitCount uint32, hashFunctions int) (*TBloomFilter, error) {
if bitCount == 0 {
return nil, errors.New("bit count must be greater than 0")
}
if hashFunctions <= 0 {
return nil, errors.New("hash function count must be greater than 0")
}
// 计算需要多少个 uint32 才能容纳 bitCount 位
size := (bitCount + 31) / 32
return &TBloomFilter{
bitArray: make([]uint32, size),
bitCount: bitCount,
hashFunctions: hashFunctions,
}, nil
}
// bitIndexToSlot 返回位索引对应的槽位(uint32 下标)以及内部位偏移
func (bf *TBloomFilter) bitIndexToSlot(index uint32) (slot uint32, mask uint32) {
slot = index / 32
mask = 1 << (index % 32)
return
}
// setBit 把指定位置 1
func (bf *TBloomFilter) setBit(index uint32) {
slot, mask := bf.bitIndexToSlot(index)
if slot < uint32(len(bf.bitArray)) {
bf.bitArray[slot] |= mask
}
}
// getBit 读取指定位
func (bf *TBloomFilter) getBit(index uint32) bool {
slot, mask := bf.bitIndexToSlot(index)
if slot >= uint32(len(bf.bitArray)) {
return false
}
return bf.bitArray[slot]&mask != 0
}
// computeHash 使用双重哈希+线性组合模拟 k 次哈希
func (bf *TBloomFilter) computeHash(value string, seed uint32) uint32 {
// h1 = FNV-1a
h1 := fnv.New32a()
h1.Write([]byte(value))
hash1 := h1.Sum32()
// h2 = FNV-1a 再写一次 seed 作为扰动
h2 := fnv.New32a()
h2.Write([]byte(value))
h2.Write([]byte{byte(seed), byte(seed >> 8), byte(seed >> 16), byte(seed >> 24)})
hash2 := h2.Sum32()
// 组合公式:(h1 + seed*h2) mod bitCount
combined := uint64(hash1) + uint64(seed)*uint64(hash2)
return uint32(combined % uint64(bf.bitCount))
}
// Add 插入元素
func (bf *TBloomFilter) Add(value string) {
bf.mu.Lock()
defer bf.mu.Unlock()
for i := 0; i < bf.hashFunctions; i++ {
pos := bf.computeHash(value, uint32(i))
bf.setBit(pos)
}
}
// MightContain 判断是否存在
func (bf *TBloomFilter) MightContain(value string) bool {
bf.mu.RLock()
defer bf.mu.RUnlock()
for i := 0; i < bf.hashFunctions; i++ {
pos := bf.computeHash(value, uint32(i))
if !bf.getBit(pos) {
return false
}
}
return true
}
// Clear 重置过滤器
func (bf *TBloomFilter) Clear() {
bf.mu.Lock()
defer bf.mu.Unlock()
for i := range bf.bitArray {
bf.bitArray[i] = 0
}
}
// 计算 BloomFilter 占用的内存字节数(仅位图)
func (bf *TBloomFilter) MemoryUsage() int {
return len(bf.bitArray) * 4
}测试运行
func Test_Bloom(t *testing.T) {
bf, _ := NewBloomFilter(1_000_000, 3)
bf.Add("https://zelig.cn")
fmt.Println(bf.MightContain("https://zelig.cn")) // true
fmt.Println(bf.MightContain("https://baidu.com")) // false(极高概率)
}



还没有评论,来说两句吧...