Redis 缓存击穿高并发下的“定点爆破”在分布式高并发场景下缓存是保护数据库的坚实盾牌。但如果这块盾牌在某个点上突然消失数据库就会面临灭顶之灾。这就是我们今天要聊的——缓存击穿 (Cache Breakdown)。1. 什么是缓存击穿一句话定义缓存击穿是指一个极度热点的 Key比如双 11 秒杀的商品信息在过期的瞬间成千上万的并发请求直接绕过 Redis 命中数据库导致数据库瞬间压力过大甚至宕机。核心特征定点爆发只有那一个或几个热点 Key 过期不是大面积过期。DB 压力骤增数据库连接池瞬间被占满响应时间飙升。Redis 状态良好Redis 服务本身是正常的只是由于 Key 过期无法命中。2. 解决方案深度解析为了应对这种“定点爆破”目前业内主流的解决方案有两种分布式锁方案和逻辑过期方案。方案一使用分布式锁互斥锁这是最常用的方案。当大量请求发现缓存失效时只允许一个线程去查询数据库并重建缓存其他线程等待。执行流程线程 A 发现缓存失效尝试获取分布式锁如 Redis 的SETNX。线程 A 抢锁成功查询 DB将结果写入 Redis然后释放锁。线程 B、C、D 抢锁失败进入休眠重试状态。等它们再次重试时缓存已经由线程 A 重建完成了直接从 Redis 获取。代码实现分布式锁方案在 Java 中我们通常使用StringRedisTemplate来操作 Redis。解决缓存击穿的核心逻辑是查询失败 - 获取锁 - 二次检查缓存 - 查询 DB - 重建缓存 - 释放锁。核心业务逻辑实现Javapublic Shop queryWithMutex(Long id) { String key cache:shop: id; // 1. 从Redis查询缓存 String shopJson stringRedisTemplate.opsForValue().get(key); // 2. 判断是否存在 if (StrUtil.isNotBlank(shopJson)) { // 存在直接返回 return JSONUtil.toBean(shopJson, Shop.class); } // 3. 实现缓存重建 // 3.1 获取互斥锁 String lockKey lock:shop: id; Shop shop null; try { boolean isLock tryLock(lockKey); // 3.2 判断是否获取成功 if (!isLock) { // 失败则休眠并重试 Thread.sleep(50); return queryWithMutex(id); // 递归重试 } // 3.3 成功获取锁后应该再次检测缓存是否存在双重检查防止前一个线程刚建好 shopJson stringRedisTemplate.opsForValue().get(key); if (StrUtil.isNotBlank(shopJson)) { return JSONUtil.toBean(shopJson, Shop.class); } // 3.4 根据id查询数据库 shop getById(id); // 模拟重建延时 Thread.sleep(200); // 3.5 不存在返回错误或写入空值解决缓存穿透 if (shop null) { stringRedisTemplate.opsForValue().set(key, , 2L, TimeUnit.MINUTES); return null; } // 3.6 写入Redis stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), 30L, TimeUnit.MINUTES); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { // 4. 释放互斥锁 unlock(lockKey); } return shop; }锁的操作工具方法利用 Redis 的SETNX(set if not exists) 特性来实现简单的分布式锁。Java// 获取锁 private boolean tryLock(String key) { // setIfAbsent 等同于 SETNX // 设置10秒过期时间防止程序崩溃导致死锁 Boolean flag stringRedisTemplate.opsForValue().setIfAbsent(key, 1, 10, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); } // 释放锁 private void unlock(String key) { stringRedisTemplate.delete(key); }优点保证了数据的强一致性不会有冗余的数据库请求。缺点性能有损耗其他线程需要等待可能导致请求堆积。方案二逻辑过期不设置 Redis 过期时间这种方案不给 Key 设置真正的 TTL而是将过期时间存在 Value 中。执行流程线程 A 发现缓存中的“逻辑过期时间”已到。线程 A 尝试获取互斥锁。抢锁成功后线程 A 开启一个新线程去异步执行 DB 查询和缓存重建自己先返回旧数据。抢锁失败后线程 B 不等待直接返回旧数据虽然数据稍微旧一点但保证了系统可用性。当然有。逻辑过期Logical Expiration的核心思想是物理上不设置过期时间而是在 Value 中维护一个逻辑时间字段。这样做的好处是当数据过期时线程不需要阻塞等待而是直接返回“旧”数据并异步开启一个后台线程去更新缓存。代码实现逻辑过期方案定义数据包装类为了不修改原始的 POJO实体类我们创建一个包装类来存放逻辑过期时间。JavaData public class RedisData { private LocalDateTime expireTime; // 逻辑过期时间 private Object data; // 存放实际的实体对象如 Shop, User 等 }核心业务实现这里使用了线程池来执行异步更新任务避免频繁创建线程的开销。Java// 创建固定大小的线程池 private static final ExecutorService CACHE_REBUILD_EXECUTOR Executors.newFixedThreadPool(10); public Shop queryWithLogicalExpire(Long id) { String key cache:shop: id; // 1. 从 Redis 查询缓存 String json stringRedisTemplate.opsForValue().get(key); // 2. 判断是否命中 if (StrUtil.isBlank(json)) { // 未命中直接返回 null逻辑过期前提是热点数据已预热进缓存 return null; } // 3. 命中反序列化为 RedisData 对象 RedisData redisData JSONUtil.toBean(json, RedisData.class); Shop shop JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class); LocalDateTime expireTime redisData.getExpireTime(); // 4. 判断是否过期 if (expireTime.isAfter(LocalDateTime.now())) { // 4.1 未过期直接返回数据 return shop; } // 5. 已过期尝试缓存重建 // 5.1 获取互斥锁 String lockKey lock:shop: id; boolean isLock tryLock(lockKey); // 5.2 判断获取锁是否成功 if (isLock) { // 成功开启独立线程进行异步更新 CACHE_REBUILD_EXECUTOR.submit(() - { try { // 重建缓存查询DB并写入Redis this.saveShopToRedis(id, 20L); // 模拟逻辑过期时间为20秒 } catch (Exception e) { throw new RuntimeException(e); } finally { // 释放锁 unlock(lockKey); } }); } // 5.3 无论是否获取锁成功先返回旧的数据 return shop; } /** * 辅助方法将查询结果封装并写入Redis */ public void saveShopToRedis(Long id, Long expireSeconds) { // 1. 查询数据库 Shop shop getById(id); // 2. 封装逻辑过期数据 RedisData redisData new RedisData(); redisData.setData(shop); redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds)); // 3. 写入 Redis不设物理过期时间 stringRedisTemplate.opsForValue().set(cache:shop: id, JSONUtil.toJsonStr(redisData)); }优点性能极佳用户无需等待适用于对数据一致性要求不那么严苛的场景。缺点可能会读到短暂的脏数据。3. 补充实战中的小技巧除了上述两种核心方案在生产环境下我们通常还会配合以下策略热点数据预热 在抢购、大促开始前通过脚本手动将热点 Key 写入 Redis。适时延长过期时间 监控热点 Key 的访问频率Hotkeys如果发现某个 Key 访问依然火爆动态延长其过期时间类似一种“自动续期”机制。多级缓存 使用本地内存缓存如 Caffeine 或 Guava Cache作为第一道防线即使 Redis 挂了本地缓存也能抗住一部分压力。4. 深度对比与选型建议维度分布式锁 (Mutex)逻辑过期 (Logical)一致性强一致最新数据最终一致可能有暂时的旧数据可用性/性能较差线程需要等待有阻塞极好零等待吞吐量高实现复杂度简单较复杂涉及包装类、异步线程池内存压力较小较大不物理删除常驻内存核心卖点不允许脏数据追求极致的用户体验高可用