Bloom Filter 算是原理很简单但应用很广泛的一个算法,用途是快速判断一个元素是否存在于集合中。存在假阳性 (false postive) 现象,但不会出现假阴性 (false negative) 现象,即算法给出存在的元素有一定概率不存在,如果算法给出不存在的元素则一定不存在。
简单描述: 存在一个 m
位的 bit 数组,初始状态下所有 bit 都设置位 0。同时有 k
个不同的 hash 函数。
k
个 hash 函数对新元素进行计算,得到 k
个不同的位置,并把 bit 数组中的这些位置设为 1。k
个 hash 函数对新元素进行计算,得到 k
个不同的位置,检查 bit 数组中这些位置是否全部为 1,如果符合则返回 true。对于给定 m
位 bit 数组, 元素总数为 n
,使假阳性概率最低的 hash 函数数量 k = m/n*ln2
。在 go-zero 的实现中设置 k
为固定值 14,具体的假阳性概率可以参考 Bloom Filters - the math
redis 4.0 之后引入了 module 机制,可以很轻松地使用 RedisBloom module 实现 bloom filter 功能:
https://github.com/RedisBloom/RedisBloom/
如果基于某种原因,比如 GCP 不支持,不能直接使用 redis module,可以手动实现基于 redis 的 bloom filter。参考 go-zero 的实现(省略了一些代码):
const (
// for detailed error rate table, see http://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.html
// maps as k in the error rate table
maps = 14
setScript = `
for _, offset in ipairs(ARGV) do
redis.call("setbit", KEYS[1], offset, 1)
end
`
testScript = `
for _, offset in ipairs(ARGV) do
if tonumber(redis.call("getbit", KEYS[1], offset)) == 0 then
return false
end
end
return true
`
)
redis 中的 bit array 操作是通过 lua script 来实现的。
// New create a Filter, store is the backed redis, key is the key for the bloom filter,
// bits is how many bits will be used, maps is how many hashes for each addition.
// best practices:
// elements - means how many actual elements
// when maps = 14, formula: 0.7*(bits/maps), bits = 20*elements, the error rate is 0.000067 < 1e-4
// for detailed error rate table, see http://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.html
func New(store *redis.Redis, key string, bits uint) *Filter {
return &Filter{
bits: bits,
bitSet: newRedisBitSet(store, key, bits),
}
}
func (f *Filter) getLocations(data []byte) []uint {
locations := make([]uint, maps)
for i := uint(0); i < maps; i++ {
hashValue := hash.Hash(append(data, byte(i)))
locations[i] = uint(hashValue % uint64(f.bits))
}
return locations
}
func (r *redisBitSet) buildOffsetArgs(offsets []uint) ([]string, error) {
var args []string
for _, offset := range offsets {
if offset >= r.bits {
return nil, ErrTooLargeOffset
}
args = append(args, strconv.FormatUint(uint64(offset), 10))
}
return args, nil
}
func (r *redisBitSet) check(offsets []uint) (bool, error) {
args, err := r.buildOffsetArgs(offsets)
if err != nil {
return false, err
}
resp, err := r.store.Eval(testScript, []string{r.key}, args)
if err == redis.Nil {
return false, nil
} else if err != nil {
return false, err
}
exists, ok := resp.(int64)
if !ok {
return false, nil
}
return exists == 1, nil
}
func (r *redisBitSet) set(offsets []uint) error {
args, err := r.buildOffsetArgs(offsets)
if err != nil {
return err
}
_, err = r.store.Eval(setScript, []string{r.key}, args)
if err == redis.Nil {
return nil
}
return err
}
可以观察到 getLocation
函数中的 k
个不同 hash 函数是通过同样的 hash 函数每次循环在 data
末尾增加一个确定的字节来实现的,这里选择了 murmur3
作为 hash 函数。
func Hash(data []byte) uint64 {
return murmur3.Sum64(data)
}
几种常见的非密码学 hash 函数: