Redis 布隆过滤器
布隆过滤器主要解决的是缓存穿透、空间效率的问题。以下是具体说明:
介绍
缓存穿透
当缓存失效,大量无效请求直接查询数据库,导致数据库压力增加,降低了系统的整体性能。
通常发生在请求的 ID 不存在于数据库中,尤其是当这些 ID 是随机生成或被恶意攻击时。
布隆过滤器通过在查询前检查请求的 ID 是否存在于布隆过滤器中,能有效地阻止无效请求。
如果布隆过滤器返回该 ID 不存在,可以立即拒绝查询请求,从而避免无效的数据库访问。
空间效率
-
布隆过滤器:
布隆过滤器以位数组(bit array)和多个哈希函数来表示集合元素,而不需要存储实际数据。具有很高的空间效率,即使存储大量元素所需的内存也相对较少。例如,存储 10 亿个元素可能只需要 1.2 GB 的内存(误判率为 1%),远比直接存储每个元素要节省空间。 -
直接存储 ID:
如果将所有 ID 直接存储在 Redis 中,比如用哈希表或集合,存储 10 亿个元素可能会占用几倍甚至十倍以上的内存。
每个元素都需要完整存储,并且包括额外的 Redis 元数据,导致内存占用更高。
性能
布隆过滤器可以在常数时间内𝑂(𝑘)(其中𝑘是哈希函数的数量)完成存在性判断,性能更高。
而 Redis 需要根据实际数据结构进行查询,性能上可能略有差异。
误判率
存在误判率,可能会认为某个 ID 存在即使它实际上不存在。
这意味着它有小概率误认为一些数据存在,但实际上不存在,从而先查询redis,再查询mysql。
适用场景
适用于对空间效率要求高、允许少量误判的场景 *防止缓存穿透**、**大数据去重**、**爬虫Url去重复**
那么开始干吧!
环境搭建
- 使用 Docker 运行 Redis + RedisBloom 容器
RedisBloom 官方提供了一个集成了 RedisBloom 模块的 Docker 镜像。
可以使用以下命令来启动 Redis 实例,并确保使用 RedisBloom 模块:docker run -d --name redis-bloom \ -p 6379:6379 \ redislabs/rebloom:latest
--name redis-bloom: 为容器命名为 redis-bloom。
-p 6379:6379: 将本地主机的 6379 端口映射到容器的 6379 端口。
redislabs/rebloom:latest: 使用 RedisBloom 的官方最新镜像。
- 验证 RedisBloom 是否已正确加载
进入容器:
docker exec -it redis-bloom /bin/bash
连接Redis:
redis-cli
验证是否已加载Bloom:
MODULE LIST
可以看到 redisbloom 模块已加载。
> MODULE LIST
1) 1) "name"
2) "bf"
3) "ver"
4) (integer) 20612
5) "path"
6) "/usr/lib/redis/modules/redisbloom.so"
7) "args"
8) (empty array)
如此,环境就搭建好了。
使用布隆过滤器
| 指令 | 描述 |
|---|---|
BF.RESERVE |
创建布隆过滤器,指定容量和误判率 |
BF.ADD |
添加一个元素 |
BF.MADD |
批量添加多个元素 |
BF.EXISTS |
检查一个元素是否存在 |
BF.MEXISTS |
批量检查多个元素是否存在 |
BF.INFO |
获取布隆过滤器的详细信息 |
BF.RESERVE
描述: 创建一个新的布隆过滤器,指定初始容量和错误率。
格式:
BF.RESERVE key error_rate capacity [EXPANSION expansion] [NONSCALING]
参数解释:
- key: 布隆过滤器的键名。
- error_rate: 目标误判率(例如,0.01 表示 1%)。
- capacity: 过滤器的初始容量,即期望插入的元素数量。
- EXPANSION (可选): 设置过滤器的增长因子,决定过滤器何时扩容。默认值为 2,表示当过滤器达到容量时,位数组扩容为原来的 2 倍。
- NONSCALING (可选): 如果提供此参数,过滤器将不会扩容,当达到容量时将不再接受新的插入。
返回值: 创建成功返回 OK。
注意:
误判率越小,误判概率越小,但空间占用也越多。
例子:创建一个key为 bf_user_id 的布隆过滤器,错误率 1%, 初始容量 10000
> BF.RESERVE bf_user_id 0.01 10000
OK
> type bf_user_id
MBbloom--
BF.ADD
描述: 向布隆过滤器中添加一个元素。
格式:
BF.ADD key item
key:布隆过滤器的键名item:添加元素内容
例子:添加数字1和字符串2
> bf.Add bf_user_id 1
(integer) 1
> bf.Add bf_user_id "2"
(integer) 1
BF.MADD
描述: 批量向布隆过滤器添加多个元素。
格式:
BF.MADD key item1 item2
key: 布隆过滤器的键名。item1 item2...: 要添加的多个元素
例子:向bf_user_id 中添加3,4,5
> bf.madd bf_user_id 3 4 5
1) (integer) 1
2) (integer) 1
3) (integer) 1
BF.EXISTS
描述: 检查一个元素是否在布隆过滤器中。
格式:
BF.EXISTS key item
key: 布隆过滤器的键名。item: 元素
返回值: 存在返回 1,不存在返回 0。
例子:检查 bf_user_id 中是否存在 1
> BF.exists bf_user_id 1
(integer) 1
此处返回1,所以存在
BF.MEXISTS
描述: 批量检查多个元素是否在布隆过滤器中。
格式:
BF.MEXISTS key item1 item2
key: 布隆过滤器的键名。item1 item2 ...: 要检查的多个元素
返回值: 对每个元素返回 1 或 0,表示元素是否存在。
bf.mexists bf_user_id 5 6
1) (integer) 1
2) (integer) 0
BF.INFO
描述: 获取布隆过滤器的详细信息,例如容量、扩容情况、误判率等。
格式:
BF.INFO key
key: 布隆过滤器的键名。
返回值: 返回一个包含过滤器信息的数组,包括:
Capacity: 当前容量。Size: 位数组的大小。Number of filters:过滤器链的数量(如果扩容过)。Number of items inserted: 已插入元素的数量。Expansion rate: 扩容因子。
例子:
> bf.info user_id
1) Capacity
2) (integer) 10000
3) Size
4) (integer) 13888
5) Number of filters
6) (integer) 1
7) Number of items inserted
8) (integer) 0
9) Expansion rate
10) (integer) 2
Capacity: 10000Size: 位数字大小 13888Number of filters:过滤器链的数量(如果扩容过)。Number of items inserted: 已插入元素的数量。Expansion rate: 扩容因子。
Golang中使用
常量
- 指令
const REDIS_ORDER_BF_RESERVE = "BF.RESERVE" // BF.RESERVE const REDIS_ORDER_BF_ADD = "BF.ADD" // BF.ADD const REDIS_ORDER_BF_MADD = "BF.MADD" // BF.MADD const REDIS_ORDER_BF_EXISTS = "BF.EXISTS" // BF.EXISTS const REDIS_ORDER_BF_MEXISTS = "BF.MEXISTS" // BF.MEXISTS const REDIS_ORDER_BF_INFO = "BF.INFO" // BF.INFO - 报错
const REDIS_ERR_ErrItemExists = "ERR item exists" // BF.RESERVE 时如果过滤器存在,会提示这个错误
初始化 redis 连接
这里简化了初始化连接,生产中应当再设置MinIdleConns,PoolSize,ReadTimeout,WriteTimeout,IdleTimeout,PoolTimeout,
package redis_test
import (
"context"
"fmt"
"sync"
"github.com/redis/go-redis/v9"
)
var once sync.Once
var Redis *redis.Client
var (
host = "127.0.0.1"
port = "6379"
username = ""
password = ""
db = 0
)
func InitRedisClient() {
once.Do(newRedisClient)
}
func newRedisClient() {
Redis = redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%s", host, port),
Username: username,
Password: password,
DB: db,
})
pong, err := Redis.Ping(context.Background()).Result()
if err != nil {
panic("初始化redis失败")
}
fmt.Println(pong)
}
定义布隆过滤器对象
// redis 的布隆过滤器
type RedisBloomFilter struct {
Ctx context.Context
Key string // 过滤器键
ErrRate float64 // 误判率
Cap int // 容量
}
// new 一个 bf 对象
func NewRedisBloomFilter(ctx context.Context, key string) *RedisBloomFilter {
return &RedisBloomFilter{
Ctx: ctx,
Key: key,
}
}
创建过滤器
// 创建 Bloom Filter
func (bf *RedisBloomFilter) Reserve() {
// bf.reserve key err_rate cap
err := Redis.Do(bf.Ctx, REDIS_ORDER_BF_RESERVE, bf.Key, bf.ErrRate, bf.Cap).Err()
if err != nil && err.Error() != REDIS_ERR_ErrItemExists {
panic(fmt.Sprintf("创建新布隆过滤器失败,err:%v", err))
}
}
添加
// 向Bloom Filter中添加
func (bf *RedisBloomFilter) Add(item interface{}) {
if item == nil {
return
}
err := Redis.Do(bf.Ctx, REDIS_ORDER_BF_ADD, bf.Key, item).Err()
if err != nil {
panic(err)
}
}
批量添加
// 批量向Bloom Filter添加 item
func (bf *RedisBloomFilter) MulAdd(items []interface{}) {
if len(items) == 0 {
return
}
options := []interface{}{REDIS_ORDER_BF_MADD, bf.Key}
options = append(options, items...)
err := Redis.Do(bf.Ctx, options...).Err()
if err != nil {
panic(err)
}
}
校验存在性
// 单个item 在布隆过滤器中的存在性
func (bf *RedisBloomFilter) BfExists(item interface{}) bool {
result, err := Redis.Do(bf.Ctx, REDIS_ORDER_BF_EXISTS, bf.Key, item).Result()
if err != nil {
panic(err)
}
if result == nil {
return false
}
if reflect.TypeOf(result).Kind() == reflect.Bool {
return result.(bool)
} else {
return false
}
}
批量校验存在性
// 批量存在性校验
func (bf *RedisBloomFilter) MulExists(items []interface{}) []bool {
exists := make([]bool, len(items))
if len(items) == 0 {
return exists
}
options := []interface{}{REDIS_ORDER_BF_MEXISTS, bf.Key}
options = append(options, items...)
results, err := Redis.Do(bf.Ctx, options...).Result()
if err != nil {
panic(err)
}
if results == nil {
return exists
}
// 循环
for i, v := range results.([]interface{}) {
if reflect.TypeOf(v).Kind() == reflect.Bool {
exists[i] = v.(bool)
} else {
exists[i] = false
}
}
return exists
}
使用
func TestRedisBloomFilter(t *testing.T) {
// 连接 redis
InitRedisClient()
// Fan的小破站 fanfine.cn
ctx, _ := context.WithTimeoutCause(context.Background(), time.Second*20, errors.New("操作超时"))
bf := NewRedisBloomFilter(ctx, "user_id")
bf.ErrRate = 0.01 // 误判率
bf.Cap = 10 ^ 8 // 总量
// 创建过滤器
bf.Reserve()
// 添加一个 1
bf.Add(1)
// 校验存在性
fmt.Println("单个校验存在性", bf.BfExists(1))
// 批量添加 2 3 4 5
ids := []interface{}{2, 3, 4, 5}
bf.MulAdd(ids)
// 批量验证存在性
checkIds := []interface{}{1, 2, "44", 2112}
exists := bf.MulExists(checkIds)
for i, exist := range exists {
fmt.Println("id:", checkIds[i], "存在性:", exist)
}
}
输出:
PONG
单个校验存在性 true
id: 1 存在性: true
id: 2 存在性: true
id: 44 存在性: false
id: 2112 存在性: false

