bloom原理
- 分配
m位的向量v,最初全部设置为0 - 选择
k个独立的哈希函数 - 对于每个函数:
- 当我们设置值
a的时候v中的位置h1(a), h2(a), ..., hk(a)处的位置设为1
- 当我们设置值
- 对于值
b的查询:- 我们检查
h1(b), h2(b), ..., hk(b)处的值- 如果其中任何一个位的值为
0,则b肯定不存在于集合中,否则,我们推断 b 在集合中
- 如果其中任何一个位的值为
- 我们检查
布隆过滤器有一定的误判,这个大家自己查资料
使用方法:
package main
import (
"context"
"fmt"
"github.com/zeromicro/go-zero/core/bloom"
"github.com/zeromicro/go-zero/core/stores/redis"
)
func main() {
store := redis.MustNewRedis(redis.RedisConf{
Host: "192.168.0.112:6379",
Pass: "nil",
Type: "node",
})
ctx := context.Background()
filter := bloom.New(store, "testbloom", 64)
filter.AddCtx(ctx, []byte("kevin"))
filter.AddCtx(ctx, []byte("wan"))
fmt.Println(filter.ExistsCtx(ctx, []byte("kevin")))
fmt.Println(filter.ExistsCtx(ctx, []byte("wan")))
fmt.Println(filter.ExistsCtx(ctx, []byte("nothing")))
}
上面这些不解释了
源码分析
整个 bloom 的代码结构如下
可以看到,bloom包对外仅提供两个方法:
Add- 向集合中添加一个元素
Exists- 判断某个元素是否存在
具体实现
type (
// A Filter is a bloom filter.
Filter struct {
bits uint
bitSet bitSetProvider
}
bitSetProvider interface {
check(ctx context.Context, offsets []uint) (bool, error)
set(ctx context.Context, offsets []uint) error
}
)
这段代码定义了接口 bitSetProvider, 然后Filter中的 bitSet 是 bitSetProvider 类型,在go-zero中,使用了redis 作为布隆过滤器
接下来我们看 New 方法:
// 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
const maps = 14
// 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),
}
}
go-zero中使用了14个哈希函数,这里建议的最佳实践 20 * 预估的元素数量,可以让错误率为 0.000067 < 1e-4。这里的 bitSet 初始化了一个 redisSet
可以具体看看:
redisSet
redisSet 实现了 bitSetProvider 接口。
我们看看具体的实现
func (r *redisBitSet) set(ctx context.Context, offsets []uint) error {
args, err := r.buildOffsetArgs(offsets)
if err != nil {
return err
}
_, err = r.store.ScriptRunCtx(ctx, setScript, []string{r.key}, args)
if err == redis.Nil {
return nil
}
return err
}
这里会调用 buildOffsetArgs 方法,将传进来的 offsets 转换为 字符串类型的 offset
然后执行 redis.ScriptRunCtx
setScript = redis.NewScript(`
for _, offset in ipairs(ARGV) do
redis.call("setbit", KEYS[1], offset, 1)
end
`
这里使用了 lua 脚本来设置 bitmap 对应的位数的值为 1
这里再附带一点lua脚本的好处:
- 减少网络开销: 在Redis操作需求需要向Redis发送5次请求,而使用脚本功能完成同样的操作只需要发送一个请求即可,减少了网络往返时延。
- 原子操作: Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。换句话说在编写脚本的过程中无需担心会出现竞态条件,也就无需使用事务。事务可以完成的所有功能都可以用脚本来实现。
- 复用: 客户端发送的脚本会永久存储在Redis中,这就意味着其他客户端(可以是其他语言开发的项目)可以复用这一脚本而不需要使用代码完成同样的逻辑。
- 速度快:见 与其它语言的性能比较, 还有一个 JIT编译器可以显著地提高多数任务的性能; 对于那些仍然对性能不满意的人, 可以把关键部分使用C实现, 然后与其集成, 这样还可以享受其它方面的好处。
命令格式
EVAL script numkeys key [key ...] arg [arg ...]
demo:
# demo1
redis> eval '
for _, offset in ipairs(ARGV) do
if tonumber(redis.call("getbit", KEYS[1], offset)) == 0 then
return false
end
end
return true
' 2 testbloom 11 0
输出
1
# demo2
redis> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
输出
1) "key1"
2) "key2"
3) "first"
4) "second"
好的,现在我们继续回到源码阅读
现在看看 exists方法:
func (r *redisBitSet) check(ctx context.Context, offsets []uint) (bool, error) {
args, err := r.buildOffsetArgs(offsets)
if err != nil {
return false, err
}
resp, err := r.store.ScriptRunCtx(ctx, 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
}
代码逻辑也很简单,首先把offset转化为 string 类型的 args
然后执行script:
testScript = redis.NewScript(`
for _, offset in ipairs(ARGV) do
if tonumber(redis.call("getbit", KEYS[1], offset)) == 0 then
return false
end
end
return true
`)
// 建议改名为 checkScript
这里会检查每一个 offset 的值是否为 0,如果为 0 的话,则返回 false,在 redis 中的表现则是 null,go 中表现则是 redis.nil
源码中写的是
resp, err := r.store.ScriptRunCtx(ctx, testScript, []string{r.key}, args)
if err == redis.Nil {
return false, nil
}
如果是 nil ,则返回 fase,该值不存在