Redis 缓存穿透、击穿与雪崩:体系化防护方案的生产级实践

Redis 缓存穿透、击穿与雪崩:体系化防护方案的生产级实践 Redis 缓存穿透、击穿与雪崩体系化防护方案的生产级实践一、缓存三大故障的根因剖析从现象到本质Redis 缓存在高并发系统中承担着 80% 以上的读取流量一旦缓存层出现故障请求将直接穿透到数据库轻则响应变慢重则数据库连接池耗尽导致系统崩溃。缓存故障有三种典型模式每种模式的根因和防护策略截然不同。缓存穿透请求查询的数据在缓存和数据库中都不存在每次请求都会穿透到数据库。典型场景是恶意攻击者用大量不存在的 ID 发起查询或者业务逻辑缺陷导致频繁查询空值。穿透的根因不是缓存失效而是缓存根本没有数据可缓存。缓存击穿某个热点 Key 在缓存过期的瞬间大量并发请求同时到达这些请求发现缓存失效后全部穿透到数据库加载。击穿的根因是热点 Key 的过期时间集中在一个时间点且没有互斥机制防止并发重建。雪崩大量 Key 在同一时间集中过期或者 Redis 节点宕机导致大量请求同时穿透到数据库。雪崩的根因是 Key 过期时间的集中性或者缓存基础设施的单点故障。三种故障的本质区别在于穿透是无数据可缓存击穿是单点热点过期雪崩是大面积过期或宕机。防护方案必须针对各自的根因设计而非一刀切。二、三层防护体系的机制剖析flowchart TD CLIENT[客户端请求] -- GATEWAY[网关层br/布隆过滤器前置拦截] GATEWAY --|合法请求| CACHE_CHECK{Redis 缓存查询} GATEWAY --|非法请求| REJECT[返回空值/拒绝] CACHE_CHECK --|命中| RETURN[返回缓存数据] CACHE_CHECK --|未命中| LOCK_CHECK{互斥锁检查} LOCK_CHECK --|获取锁成功| DB_QUERY[查询数据库] LOCK_CHECK --|获取锁失败| WAIT[等待并重试br/自旋50ms] DB_QUERY --|数据存在| WRITE_CACHE[回写缓存br/随机过期时间] DB_QUERY --|数据不存在| WRITE_NULL[写入空值缓存br/短过期时间] WRITE_CACHE -- RETURN WRITE_NULL -- RETURN_EMPTY[返回空值] subgraph 雪崩防护 RANDOM_TTL[随机过期时间br/baseTTL random(0, 300s)] HA_CLUSTER[Redis Clusterbr/主从 哨兵] end WRITE_CACHE -- RANDOM_TTL CACHE_CHECK -.- HA_CLUSTER style GATEWAY fill:#e74c3c,color:#fff style LOCK_CHECK fill:#e67e22,color:#fff style RANDOM_TTL fill:#27ae60,color:#fff style HA_CLUSTER fill:#3498db,color:#fff第一层——布隆过滤器穿透防护在请求到达 Redis 之前先通过布隆过滤器判断查询的 Key 是否可能存在。布隆过滤器是一个空间效率极高的概率型数据结构如果判断 Key 不存在则一定不存在如果判断 Key 存在则可能存在有误判率。将所有合法数据的 ID 预加载到布隆过滤器中可以拦截绝大部分穿透请求。第二层——互斥锁重建击穿防护当缓存失效时只允许一个线程查询数据库并重建缓存其他线程等待缓存重建完成后直接读取缓存。互斥锁通过 Redis 的SETNX命令实现设置超时时间防止死锁。第三层——随机过期时间与高可用集群雪崩防护为每个 Key 的过期时间添加随机偏移量避免大量 Key 在同一时间过期。同时部署 Redis Cluster 主从集群确保单节点宕机不影响整体可用性。三、生产级代码实现3.1 布隆过滤器防穿透/** * 基于 Redisson 布隆过滤器的穿透防护 * 核心设计数据写入时同步更新布隆过滤器 * 查询时先校验布隆过滤器不存在的 Key 直接拒绝 */ Service Slf4j public class BloomFilterProtection { private final RBloomFilterLong itemBloomFilter; public BloomFilterProtection(RedissonClient redissonClient) { // 初始化布隆过滤器 // expectedInsertions: 预期插入量设为 1000 万 // falseProbability: 误判率设为 0.011% // 误判率越低内存占用越大1% 是性价比较高的选择 this.itemBloomFilter redissonClient.getBloomFilter( itemBloomFilter ); this.itemBloomFilter.tryInit(10_000_000L, 0.01); } /** * 查询前校验Key 是否可能存在 * 布隆过滤器返回 false 表示一定不存在直接拒绝 * 返回 true 表示可能存在继续查询缓存和数据库 */ public boolean mightExist(Long itemId) { boolean exists itemBloomFilter.contains(itemId); if (!exists) { log.info(布隆过滤器拦截, itemId{}, itemId); // 记录拦截指标用于监控穿透率 PENETRATION_REJECT_COUNTER.inc(); } return exists; } /** * 数据写入时更新布隆过滤器 * 必须在数据库写入成功后同步更新保证一致性 */ public void addToBloomFilter(Long itemId) { itemBloomFilter.add(itemId); log.debug(布隆过滤器更新, itemId{}, itemId); } }3.2 互斥锁防击穿/** * 互斥锁缓存重建器 * 核心设计缓存失效时通过 SETNX 争抢锁 * 获取锁的线程负责查库重建缓存其他线程等待后重试 */ Service Slf4j public class MutexCacheRebuilder { Autowired private RedisTemplateString, Object redisTemplate; Autowired private ItemMapper itemMapper; // 锁的过期时间10 秒防止死锁 // 设为 10 秒是因为数据库查询 缓存回写通常在 1 秒内完成 // 但要留出足够余量避免锁提前释放导致并发重建 private static final long LOCK_EXPIRE_SECONDS 10; // 等待重试的间隔50 毫秒 // 不宜过短CPU 空转不宜过长增加延迟 private static final long RETRY_INTERVAL_MS 50; // 最大等待时间3 秒 // 超过此时间直接返回空值避免用户长时间等待 private static final long MAX_WAIT_MS 3000; public Item getItemWithMutex(Long itemId) { String cacheKey item: itemId; String lockKey lock:item: itemId; // 1. 查询缓存 Item item (Item) redisTemplate.opsForValue().get(cacheKey); if (item ! null) { return item; } // 2. 缓存未命中尝试获取互斥锁 long startTime System.currentTimeMillis(); while (System.currentTimeMillis() - startTime MAX_WAIT_MS) { // SETNX 尝试获取锁 Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, LOCK_EXPIRE_SECONDS, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { // 获取锁成功负责重建缓存 try { // 双重检查获取锁后再次确认缓存是否已被其他线程重建 item (Item) redisTemplate.opsForValue() .get(cacheKey); if (item ! null) { return item; } // 查询数据库 item itemMapper.selectById(itemId); if (item ! null) { // 回写缓存随机过期时间防雪崩 long ttl 3600 ThreadLocalRandom.current() .nextLong(0, 600); redisTemplate.opsForValue().set( cacheKey, item, ttl, TimeUnit.SECONDS ); } else { // 空值缓存防止穿透短过期时间 redisTemplate.opsForValue().set( cacheKey, NULL, 60, TimeUnit.SECONDS ); } return item; } finally { // 释放锁 redisTemplate.delete(lockKey); } } // 获取锁失败等待后重试 try { Thread.sleep(RETRY_INTERVAL_MS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return null; } } // 等待超时返回降级数据 log.warn(互斥锁等待超时, itemId{}, itemId); return null; } }3.3 随机过期时间防雪崩/** * 缓存写入工具类 * 核心设计所有缓存写入都通过此工具类自动添加随机过期时间 * 避免开发人员直接调用 RedisTemplate 导致过期时间集中 */ Component public class CacheWriteUtil { Autowired private RedisTemplateString, Object redisTemplate; // 基础过期时间1 小时 private static final long BASE_TTL_SECONDS 3600; // 随机偏移范围0-600 秒10 分钟 // 这样即使同一时刻写入的 Key过期时间也分散在 1h-1h10min 之间 private static final long RANDOM_OFFSET_SECONDS 600; /** * 写入缓存自动添加随机过期时间 * param key 缓存 Key * param value 缓存值 */ public void putWithRandomTTL(String key, Object value) { long ttl BASE_TTL_SECONDS ThreadLocalRandom.current().nextLong(RANDOM_OFFSET_SECONDS); redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS); } /** * 写入空值缓存使用较短的过期时间 * 空值缓存是为了防穿透不需要长期占用内存 * param key 缓存 Key */ public void putNullValue(String key) { // 空值缓存 60 秒过期足够防穿透又不会长期占用内存 redisTemplate.opsForValue().set(key, NULL, 60, TimeUnit.SECONDS); } /** * 批量写入缓存 * 使用 Pipeline 减少网络往返提升批量写入性能 */ public void batchPutWithRandomTTL(MapString, Object entries) { redisTemplate.executePipelined((RedisCallbackObject) connection - { for (Map.EntryString, Object entry : entries.entrySet()) { long ttl BASE_TTL_SECONDS ThreadLocalRandom.current() .nextLong(RANDOM_OFFSET_SECONDS); byte[] keyBytes redisTemplate.getStringSerializer() .serialize(entry.getKey()); byte[] valueBytes redisTemplate.getValueSerializer() .serialize(entry.getValue()); connection.setEx(keyBytes, ttl, valueBytes); } return null; }); } }四、防护方案的代价内存开销、延迟增加与一致性窗口布隆过滤器的内存开销1000 万条数据、1% 误判率的布隆过滤器约占用 12MB 内存。误判率降到 0.1% 时内存占用翻倍。此外布隆过滤器不支持删除操作——如果数据被物理删除布隆过滤器中的标记无法移除只能重建整个过滤器。对于频繁删除的数据场景需要改用 Counting Bloom Filter但内存占用增加 4 倍。互斥锁的延迟增加缓存失效时未获取锁的线程需要等待 50ms 后重试。在极端情况下大量并发请求同时到达等待时间可能累积到秒级。对于延迟敏感的场景可以使用逻辑过期方案替代互斥锁——缓存永不过期但在值中存储逻辑过期时间发现逻辑过期后异步更新缓存用户始终读到旧数据但延迟为零。空值缓存的内存浪费如果攻击者使用大量不同的无效 ID 发起查询空值缓存会占用大量 Redis 内存。解决方案是对空值缓存设置较短的过期时间30-60 秒并配合布隆过滤器前置拦截减少空值缓存的数量。五、总结缓存穿透、击穿和雪崩是三种根因不同的故障模式防护策略必须对症下药。布隆过滤器在入口层拦截不存在的 Key从根源上消除穿透互斥锁确保缓存失效时只有一个线程重建缓存避免击穿引发的数据库压力随机过期时间和高可用集群分别从时间维度和基础设施维度防止雪崩。三层防护协同工作才能构建真正可靠的缓存防线。落地路线建议第一步梳理业务中的热点 Key为热点数据配置互斥锁重建策略第二步实现布隆过滤器组件在数据写入时同步更新过滤器第三步统一缓存写入工具类强制所有缓存写入使用随机过期时间第四步部署 Redis Cluster 高可用集群确保单节点故障不影响整体可用性第五步建立缓存命中率、穿透率、重建耗时的监控看板持续优化防护参数。