【缓存优化】缓存穿透:布隆过滤器(Guava/RedisBloom)

以下是针对缓存穿透 的布隆过滤器(Bloom Filter)治理方案,涵盖**本地(Guava)分布式(RedisBloom)**双模式,包含数学原理、生产级代码与容错架构。


一、缓存穿透的杀伤逻辑

1.1 攻击模型

复制代码
恶意请求 → 查询不存在Key → 绕过Cache直击DB → DB连接耗尽 → 级联雪崩

高危场景

  • 恶意遍历user?id=-1, order?oid=uuid(随机UUID撞库)
  • 历史数据清理:批量删除Key后,上游未感知继续请求
  • 参数构造 :SQL注入式试探 user?id=1' or '1'='1

传统方案缺陷

  • 缓存空值(Cache-Aside null) :需设置较短TTL(1-5分钟),且无法防御随机Key攻击(每个Key只访问一次,空值无意义)

二、布隆过滤器数学原理

2.1 空间奇迹

存储一亿条数据,仅需 ~100MB (1%误判率),而HashMap需 ~6GB

核心公式(m=位数组大小,n=元素数量,k=哈希函数数,p=误判率):

m=−nln⁡p(ln⁡2)2≈1.44⋅n⋅log⁡2(1p)m = -\frac{n \ln p}{(\ln 2)^2} \approx 1.44 \cdot n \cdot \log_2(\frac{1}{p})m=−(ln2)2nlnp≈1.44⋅n⋅log2(p1)

k=mnln⁡2≈0.69⋅mnk = \frac{m}{n} \ln 2 \approx 0.69 \cdot \frac{m}{n}k=nmln2≈0.69⋅nm

最优参数速查表(n=1亿):

误判率 § 位数组大小 (m) 哈希函数数 (k) 理论空间
0.1% (1‰) 1.44 GB 10 180 MB
1% (1%) 958 MB 7 120 MB
0.01% (万分之一) 2.88 GB 14 360 MB

注:1 Byte = 8 bits,上表已换算

2.2 假阳性与假阴性

  • 假阳性(False Positive) :BF判定存在 → 实际不存在(可接受,仅损失一次Cache查询)
  • 假阴性(False Negative) :BF判定不存在 → 实际存在(致命,导致数据丢失)

绝对约束 :BF禁止删除(Counting Bloom Filter除外),否则产生假阴性。


三、Guava BloomFilter(本地模式)

3.1 适用场景

  • 单节点部署本机缓存预热
  • 数据量<千万级(受限于JVM Heap)
  • 允许JVM重启重建(数据不持久)

3.2 生产级代码

java 复制代码
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

public class LocalBloomFilter {
    // 预计存储100万用户ID,误判率0.01%
    private static final BloomFilter<String> USER_ID_BF = 
        BloomFilter.create(
            Funnels.stringFunnel(Charset.defaultCharset()), 
            1_000_000, 
            0.0001
        );
    
    // 双过滤策略:Guava本地 + Redis远端
    public User getUser(String userId) {
        // 1. 本地BF挡截(零RTT)
        if (!USER_ID_BF.mightContain(userId)) {
            log.warn("BF拦截非法用户ID: {}", userId);
            return null; // 直接返回,不打穿透
        }
        
        // 2. Redis查询
        User user = redisTemplate.opsForValue().get("user:" + userId);
        if (user != null) return user;
        
        // 3. 查询DB(极少发生)
        user = userDao.findById(userId);
        if (user == null) {
            // 缓存空值短期防穿透(双重保险)
            redisTemplate.opsForValue().set(
                "user:" + userId, 
                "NULL", 
                60, 
                TimeUnit.SECONDS
            );
        } else {
            redisTemplate.opsForValue().set("user:" + userId, user);
        }
        return user;
    }
    
    // 数据初始化(启动时全量加载)
    @PostConstruct
    public void init() {
        List<String> allUserIds = userDao.findAllIds();
        allUserIds.forEach(USER_ID_BF::put);
        log.info("BF初始化完成, 录入{}条数据", allUserIds.size());
    }
    
    // 新增用户时同步写入BF(避免假阴性)
    public void addUser(User user) {
        userDao.save(user);
        USER_ID_BF.put(user.getId());
        redisTemplate.delete("user:" + user.getId()); // 清缓存
    }
}

3.3 进阶:Counting Bloom Filter(支持删除)

Guava未原生支持,但可通过过时标记+定时重建模拟:

java 复制代码
// 使用BitMap + Int计数器(Redis方案更优,见下文)

四、RedisBloom(分布式模式)

4.1 RedisBloom模块 vs 原生BitMap

  • 原生BitMap:需自行实现哈希函数与多Key管理,易出错
  • RedisBloom模块 (官方提供):
    • 命令式操作:BF.ADD, BF.EXISTS, BF.RESERVE
    • 支持SCANDUMP/LOAD(持久化与迁移)
    • 支持Count-Min Sketch(频率统计)

安装

bash 复制代码
redis-server --loadmodule /path/to/redisbloom.so

4.2 分布式架构设计

分层过滤架构

复制代码
Client → 本地Caffeine (L1) → RedisBloom (L2过滤) → Redis Data (L3) → DB

Java实现(Lettuce + RedisBloom):

java 复制代码
@Configuration
public class RedisBloomConfig {
    
    @Bean
    public RedisBloomClient bloomClient() {
        // 连接RedisBloom模块
        RedisURI uri = RedisURI.builder()
            .withHost("redis-cluster")
            .withPort(6379)
            .build();
        return new RedisBloomClient(uri);
    }
    
    // 初始化布隆过滤器(误判率0.1%,容量1亿)
    @PostConstruct
    public void initBloom() {
        try {
            redisBloomClient.execute("BF.RESERVE", 
                "bf:user_id",   // Key
                "0.001",        // 误判率
                "100000000"     // 初始容量(自动扩容)
            );
        } catch (Exception e) {
            // BF.EXISTS 表示已存在,忽略
        }
        
        // 全量导入(使用管道批量写入)
        batchLoadData();
    }
    
    public boolean mightContain(String userId) {
        // BF.EXISTS 返回0/1
        Long exists = (Long) redisBloomClient.execute("BF.EXISTS", 
            "bf:user_id", userId);
        return exists == 1;
    }
    
    public void addElement(String userId) {
        redisBloomClient.execute("BF.ADD", "bf:user_id", userId);
    }
}

4.3 Hot Key与大Key治理

问题:单BF Key存储亿级数据,导致:

  • Redis单线程瓶颈:BF.EXISTS成为热点
  • 大Key风险:网络传输与持久化阻塞

分片(Sharding)策略

java 复制代码
public class ShardedBloomFilter {
    private final int shardCount = 8; // 分8片
    private final String prefix = "bf:user:";
    
    public boolean mightContain(String userId) {
        int shard = hash(userId) % shardCount;
        String key = prefix + shard;
        return redisBloomClient.exists(key, userId);
    }
    
    // 使用CRC16均匀分布
    private int hash(String key) {
        return Math.abs(key.hashCode()) % shardCount;
    }
}

五、双写一致性与重建策略

5.1 数据同步机制

写路径(避免假阴性):

复制代码
DB事务提交成功后 → 立即BF.ADD → 异步刷入Redis
  • 异常处理 :若BF写入失败,记录本地日志队列,定时重试,避免同步阻塞主流程

异步重建(全量同步):

java 复制代码
// 使用Canal监听MySQL Binlog
@CanalListener
public void onUserChange(CanalEntry entry) {
    if (entry.getType() == INSERT) {
        String userId = extractId(entry);
        // 双写BF(本地+Redis)
        localBF.put(userId);
        redisBloomClient.add("bf:user_id", userId);
    }
}

5.2 定期重建(解决BF无法删除问题)

时间轮盘重建

  • 24小时 新建一个BF(bf:user:v2),写入新数据
  • 查询时双CheckBF.EXISTS bf:user:v1 && BF.EXISTS bf:user:v2
  • 48小时后删除v1,将v2改为v1(交替滚动)

布谷鸟过滤器(Cuckoo Filter)替代

若需支持删除,可替换为Cuckoo Filter(Redis 4.0+模组),支持动态删除且空间效率更高。


六、容错与降级方案

6.1 多层降级开关

java 复制代码
public class DegradableBloomFilter {
    private volatile boolean bloomEnabled = true;
    
    public User getUser(String id) {
        // 开关1:BF总开关(Redis故障时关闭)
        if (bloomEnabled) {
            try {
                if (!bloomClient.exists(id)) return null;
            } catch (Exception e) {
                // Redis故障,降级为直接查Cache+DB,告警
                bloomEnabled = false;
                alert("BF降级");
            }
        }
        
        // 开关2:本地BF兜底(分布式BF失效时启用)
        if (!bloomEnabled && !localBf.mightContain(id)) {
            return null;
        }
        
        return queryCache(id);
    }
}

6.2 缓存空值Fallback

当BF误判(极小概率)打到DB时,使用空值缓存保DB:

java 复制代码
if (user == null) {
    // 布隆过滤器误判防护(防缓存击穿)
    redisTemplate.opsForValue().set(
        "null:user:" + id, 
        "", 
        5, // 极短TTL
        TimeUnit.SECONDS
    );
    return null;
}

七、方案对比与选型矩阵

维度 Guava本地BF RedisBloom分布式 缓存空值
存储位置 JVM Heap Redis Server Redis Server
容量上限 单机内存(建议<1亿) 数十亿(可水平扩展) 取决于Key数量
一致性 单点,重启丢失 集群持久化 易过期,需续期
RTT成本 0(本地内存) 0.5-1ms(网络IO) 0.5-1ms
删除支持 不支持(需重建) 不支持(需重建) 支持(TTL删除)
适用数据 静态/准静态字典 海量动态数据 少量固定非法Key
复杂度 高(需运维Redis模块) 极低

决策树

  • QPS>10万数据量<1000万 → Guava本地 + Redis备份
  • 数据量>1亿多服务共享 → RedisBloom分片
  • 预算受限容忍短时空窗 → 缓存空值(TTL 1分钟)

八、线上实战 checklist

markdown 复制代码
□ BF初始化是否使用**批量管道**(非逐条ADD,减少RTT)
□ 是否设置BF自动扩容(RedisBloom的`NON_SCALING`设为no)
□ 误率是否压测验证(抽样1万Key人工校验)
□ 是否监控BF内存使用(`BF.INFO bf:user`查看字节数)
□ 写DB后是否**同步写BF**(假阴性会击穿缓存)
□ 是否配置**BF降级开关**(Redis故障时切空值缓存)
□ 定期重建任务是否低峰期执行(避免SCAN阻塞)
□ 分片策略是否均匀(避免Redis节点热点倾斜)

核心认知 :布隆过滤器是概率型防御 ,必须配合缓存空值 作为二道防线,且永远保留DB降级熔断(Sentinel)作为最后保障。

相关推荐
Moshow郑锴2 小时前
Spring Boot Data API 与 Redis 集成:KPI/图表/表格查询的缓存优化方案
spring boot·redis·缓存
小马爱打代码2 小时前
MyBatis:缓存体系设计与避坑大全
java·缓存·mybatis
三水不滴3 小时前
SpringBoot+Caffeine+Redis实现多级缓存
spring boot·redis·笔记·缓存
打工的小王16 小时前
redis(四)搭建哨兵模式:一主二从三哨兵
数据库·redis·缓存
春生野草19 小时前
Redis
数据库·redis·缓存
万象.1 天前
redis持久化:AOF和RDB
数据库·redis·缓存
cheungxiongwei.com1 天前
深入解析 DNS 缓存与 TTL:工作原理、修改生效机制与优化策略
缓存
!chen1 天前
Redis快速实现布隆过滤器
数据库·redis·缓存
xxxmine1 天前
Redis 持久化详解:RDB、AOF 与混合模式
数据库·redis·缓存