当前位置: 代码网 > it编程>数据库>Redis > 布隆过滤器是什么?如何用Redis防止缓存穿透

布隆过滤器是什么?如何用Redis防止缓存穿透

2026年08月18日 Redis 我要评论
一、什么是布隆过滤器布隆过滤器(bloom filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否在一个集合中。核心特性:判断为"不存在",则一定不存在(100%

一、什么是布隆过滤器

布隆过滤器(bloom filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否在一个集合中。

核心特性:

  • 判断为"不存在",则一定不存在(100% 准确)
  • 判断为"存在",则可能存在(有一定误判率)
  • 插入的元素越多,误判率越高
  • 不存储元素本身,只占用固定大小的位数组

典型场景:

场景说明
缓存穿透防护查询不存在的数据时直接拦截,不穿透到 db
爬虫 url 去重数十亿 url 去重,内存只需几 gb
黑名单/白名单无需存储完整数据,快速判断是否命中
邮件/用户名判重注册时判断用户名是否已被使用
推荐系统去重已推荐过的不再重复推荐

二、布隆过滤器原理

2.1 数据结构

位数组(bit array):    ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
                         │ 0 │ 1 │ 0 │ 0 │ 1 │ 0 │ 1 │ 0 │ 0 │ 1 │ ...
                         └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
bit 索引:                 0   1   2   3   4   5   6   7   8   9

2.2 添加元素

添加 "hello" 时:
  hash1("hello") = 2  ──► 索引 2 置 1
  hash2("hello") = 5  ──► 索引 5 置 1
  hash3("hello") = 9  ──► 索引 9 置 1

添加 "world" 时:
  hash1("world") = 1  ──► 索引 1 置 1
  hash2("world") = 4  ──► 索引 4 置 1
  hash3("world") = 8  ──► 索引 8 置 1

结果位数组:
              ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
              │ 0 │ 1 │ 1 │ 0 │ 1 │ 1 │ 0 │ 0 │ 1 │ 1 │
              └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
index:         0   1   2   3   4   5   6   7   8   9

2.3 查询元素

  • 查询 “hello” → hash 得 [2,5,9] → 全是 1 → 可能存在
  • 查询 “hello1” → hash 得 [2,3,7] → 位 3 是 0 → 一定不存在
  • 查询 “hello2” → hash 得 [1,4,9] → 全是 1 → 可能存在(但从未添加过,这就是误判)

2.4 误判率计算

误判率 p ≈ (1 - e^(-k*n/m))^k
  m = 位数组长度(bit 数)
  n = 插入元素数量
  k = 哈希函数个数

最优哈希函数个数:k ≈ (m/n) * ln(2) ≈ 0.7 * (m/n)

示例:m=10亿(128mb), n=100万,k≈7,误判率 p ≈ 千万分之一

三、方式一:redis stack / redisbloom 模块(推荐)

redis 7.x 已集成 redisbloom,无需额外安装。旧版本安装 redis stack 即可。

3.1 创建布隆过滤器

# bf.reserve <key> <error_rate> <capacity> [expansion expansion] [nonscaling]
bf.reserve user_filter 0.01 1000000
参数说明
key过滤器名称
error_rate期望误判率,越小越占内存(0~1)
capacity预计存储的元素数量
expansion容量超限后自动扩容倍数(默认 2)
nonscaling禁止自动扩容

3.2 添加元素

bf.add user_filter "user_123"          # 单个添加,返回 1 新增 / 0 可能重复
bf.madd user_filter "a" "b" "c"        # 批量添加 → [1, 1, 1]

3.3 查询元素

bf.exists user_filter "user_123"       # 单个查询,返回 1 可能存在 / 0 一定不存在
bf.mexists user_filter "a" "x" "c"     # 批量查询 → [1, 0, 1]

3.4 查看信息

bf.info user_filter    # 返回容量、已插入数量、子过滤器数量等
bf.card user_filter    # 返回独立 item 数(去重估计值)

3.5 多语言代码示例

java(jedis)

import redis.clients.jedis.unifiedjedis;

public class bloomfilterdemo {
    private static final string filter_key = "cache:bloom";

    public static void main(string[] args) {
        try (unifiedjedis jedis = new unifiedjedis("redis://localhost:6379")) {
            jedis.bfreserve(filter_key, 0.01, 1_000_000);

            jedis.bfadd(filter_key, "article_001");
            jedis.bfadd(filter_key, "article_002");

            string id = "article_999";
            if (!jedis.bfexists(filter_key, id)) {
                system.out.println(id + " 一定不存在,直接返回 null");
                return;
            }
            // 可能存在,查缓存或 db
            string cache = jedis.get("article:" + id);
            if (cache != null) return;
            // 查 db ...
        }
    }
}

go(go-redis)

import "github.com/redis/go-redis/v9"

rdb := redis.newclient(&redis.options{addr: "localhost:6379"})
rdb.bfreserve(ctx, "myfilter", 0.01, 1000000)
rdb.bfadd(ctx, "myfilter", "item1")
exists, _ := rdb.bfexists(ctx, "myfilter", "item1").result()

python(redis-py)

import redis
r = redis.redis(host='localhost', port=6379)
r.bf().reserve('myfilter', 0.01, 1000000)
r.bf().add('myfilter', 'item1')
r.bf().exists('myfilter', 'item1')   # true/false

四、方式二:bitmap + lua 脚本(纯 redis,无插件)

当无法使用 redis stack 时,用 redis 原生的 bitmap(setbit/getbit)配合 lua 手动实现。

4.1 lua 添加元素脚本

-- bloom_add.lua,keys[1]=过滤器 key,argv=要添加的元素
local key = keys[1]
local bits = 1 << 31

for i = 1, #argv do
    local val = argv[i]
    local h1 = math.abs(redis.call('hash', val) % bits)
    local h2 = math.abs(redis.call('crc16', val) % bits)
    local h3 = math.abs((h1 + h2) % bits)
    redis.call('setbit', key, h1, 1)
    redis.call('setbit', key, h2, 1)
    redis.call('setbit', key, h3, 1)
end
return 1

4.2 lua 查询元素脚本

-- bloom_exists.lua,keys[1]=过滤器 key,argv=要查询的元素,返回数组 [1,0,1,...]
local key = keys[1]
local bits = 1 << 31
local results = {}

for i = 1, #argv do
    local val = argv[i]
    local h1 = math.abs(redis.call('hash', val) % bits)
    local h2 = math.abs(redis.call('crc16', val) % bits)
    local h3 = math.abs((h1 + h2) % bits)
    if redis.call('getbit', key, h1) == 0
        or redis.call('getbit', key, h2) == 0
        or redis.call('getbit', key, h3) == 0 then
        results[i] = 0
    else
        results[i] = 1
    end
end
return results

4.3 java 调用示例

public class redisbitmapbloomfilter {

    private static final string add_script = loadscript("bloom_add.lua");
    private static final string exists_script = loadscript("bloom_exists.lua");

    private final jedispool pool;
    private final string addsha;
    private final string existssha;

    public redisbitmapbloomfilter(jedispool pool) {
        this.pool = pool;
        try (jedis jedis = pool.getresource()) {
            this.addsha = jedis.scriptload(add_script);
            this.existssha = jedis.scriptload(exists_script);
        }
    }

    public void add(string key, string... values) {
        try (jedis jedis = pool.getresource()) {
            jedis.evalsha(addsha, 1, key, values);
        }
    }

    public boolean exists(string key, string value) {
        try (jedis jedis = pool.getresource()) {
            list<long> result = (list<long>) jedis.evalsha(
                existssha, 1, key, value);
            return result != null && result.get(0) == 1;
        }
    }
}

4.4 位数组大小与内存对照

位数组大小 (bit)内存占用适用数据量 (1%误判)
2^28 (2.68 亿)32 mb~100 万
2^30 (10.7 亿)128 mb~400 万
2^32 (42.9 亿)512 mb~1600 万

五、实战:缓存穿透防护

5.1 问题

恶意攻击者用大量不存在的 id 查询 → 每次缓存未命中 → 全部落到数据库 → db 崩溃

5.2 方案架构

请求 → 布隆过滤器 → 不存在 → 直接返回 null(拦截 ✓)
                  → 可能存在 → 查缓存 → 命中 → 返回
                                      → 未命中 → 查 db → 回写缓存

5.3 spring boot 集成

@configuration
public class bloomfilterconfig {

    @bean
    public unifiedjedis unifiedjedis() {
        return new unifiedjedis("redis://localhost:6379");
    }

    @postconstruct
    public void initbloomfilter(unifiedjedis jedis, productmapper mapper) {
        jedis.bfreserve("product:bloom", 0.01, 1_000_000);
        // 预热:将已有数据 id 全部加入过滤器
        mapper.getallids().foreach(id -> jedis.bfadd("product:bloom", id));
    }
}
@service
public class productservice {

    private final unifiedjedis jedis;
    private final productmapper mapper;

    public product getbyid(string id) {
        // 1. 布隆过滤器拦截
        if (!jedis.bfexists("product:bloom", id)) {
            return null;
        }
        // 2. 缓存查询
        string cachekey = "product:" + id;
        string cached = jedis.get(cachekey);
        if (cached != null) return json.parseobject(cached, product.class);
        // 3. 数据库查询
        product p = mapper.selectbyid(id);
        if (p != null) jedis.setex(cachekey, 3600, json.tojsonstring(p));
        return p;
    }

    public void add(product p) {
        mapper.insert(p);
        jedis.bfadd("product:bloom", string.valueof(p.getid()));
    }
}

六、常见问题

q1:布隆过滤器可以删除元素吗?

标准布隆过滤器不支持删除。需删除时可使用 redisbloom 的计数布隆过滤器(cuckoo filter):

cf.reserve cfilter 0.01 1000000
cf.add cfilter "item1"      # 添加
cf.del cfilter "item1"      # 删除(支持!)
cf.exists cfilter "item1"   # 判断

q2:如何选择误判率?

场景建议误判率
缓存穿透防护1%
url / 爬虫去重0.1% ~ 1%
黑名单0.01%

q3:布隆过滤器满了怎么办?

策略说明
自动扩容(默认)容量超限自动创建子过滤器,查询时遍历全部
分层过滤按时间分片:bloom:2026-01、bloom:2026-02
手动重建监控误判率,达阈值后重建更大的过滤器

七、方案对比总结

方案优点缺点适用场景
redisbloom 模块api 简洁、高性能、自动扩容需 redis stack / 7.x生产环境首选
bitmap + lua纯 redis,零依赖实现复杂、不可扩容无法装模块的过渡方案
本地 guava单机零网络延迟多实例间不一致单机应用
redisson封装好的分布式实现底层仍是 bitmap不想手写 lua 的场景

结论:

  • 优先用 redisbloom;
  • 装不了再用 bitmap + lua 兜底。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com