当前位置: 首页 > news >正文

缓存三大劫攻防战:穿透、击穿、雪崩的Java实战防御体系(二)

第二部分:缓存击穿——热点key过期引发的“DB瞬间高压”

缓存击穿的本质是“某个热点key(高并发访问)突然过期”,导致大量请求在同一时间穿透缓存,集中冲击DB,形成“瞬间高压”。

案例3:电商秒杀的“库存超卖”惊魂

故障现场

某电商平台“618”秒杀活动中,一款限量1000台的手机采用“Redis缓存+MySQL”架构:

  • 缓存key:seckill:stock:1001(存储库存数量),过期时间1小时;
  • 流程:查询缓存→未命中则查DB→扣减库存→更新缓存。
  • 故障:活动开始1小时后,缓存key恰好过期,此时2000+用户同时刷新页面,缓存未命中,所有请求直达MySQL查询库存。MySQL因瞬间高并发(2000QPS)出现锁等待,库存更新延迟,最终超卖50台。
根因解剖
  1. 热点key(seckill:stock:1001)过期瞬间,2000+并发请求穿透至MySQL;
  2. MySQL查询库存时加行锁(SELECT stock FROM seckill WHERE item_id=1001 FOR UPDATE),并发请求排队等待,导致库存更新延迟;
  3. 前端未做防重放处理,用户多次刷新加剧并发。
三重防御方案落地
方案1:热点数据“逻辑永不过期”

核心逻辑:缓存不设置物理过期时间,而是在value中嵌入“逻辑过期时间”。当逻辑过期时,不直接删除缓存,而是通过后台线程异步更新,当前请求仍返回旧数据。
优势:彻底避免过期瞬间的并发穿透。

实战代码

@Service
public class SeckillStockService {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate SeckillMapper seckillMapper;// 线程池:处理缓存异步更新private final ExecutorService updatePool = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS,new LinkedBlockingQueue<>(100),new ThreadPoolExecutor.CallerRunsPolicy());// 缓存数据模型(含逻辑过期时间)@Datastatic class StockCache {private Integer stock; // 库存数量private long expireTime; // 逻辑过期时间(毫秒)}/*** 查询秒杀库存(逻辑永不过期)*/public Integer getStock(Long itemId) {String cacheKey = "seckill:stock:" + itemId;// 1. 查询缓存String cacheVal = redisTemplate.opsForValue().get(cacheKey);if (cacheVal == null) {// 2. 缓存未命中(首次加载):加锁查询DB并初始化return loadStockWithLock(itemId, cacheKey);}// 3. 解析缓存数据StockCache cache = JSON.parseObject(cacheVal, StockCache.class);// 4. 逻辑未过期:直接返回if (System.currentTimeMillis() < cache.getExpireTime()) {return cache.getStock();}// 5. 逻辑已过期:异步更新缓存,当前请求返回旧数据updatePool.submit(() -> refreshStockCache(itemId, cacheKey));return cache.getStock();}// 加锁加载库存(防止缓存击穿)private Integer loadStockWithLock(Long itemId, String cacheKey) {// 使用Redisson分布式锁RLock lock = redissonClient.getLock("lock:seckill:stock:" + itemId);try {// 最多等待100ms,持有锁5秒if (lock.tryLock(100, 5000, TimeUnit.MILLISECONDS)) {// 双重检查:防止重复加载String cacheVal = redisTemplate.opsForValue().get(cacheKey);if (cacheVal != null) {return JSON.parseObject(cacheVal, StockCache.class).getStock();}// 查询DB并初始化缓存(逻辑过期1小时)Integer stock = seckillMapper.selectStock(itemId);StockCache cache = new StockCache();cache.setStock(stock);cache.setExpireTime(System.currentTimeMillis() + 3600 * 1000);redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(cache));return stock;} else {// 获取锁失败:返回DB查询结果(兜底)return seckillMapper.selectStock(itemId);}} finally {if (lock.isHeldByCurrentThread()) {lock.unlock();}}}// 刷新缓存(异步执行)private void refreshStockCache(Long itemId, String cacheKey) {RLock lock = redissonClient.getLock("lock:seckill:stock:" + itemId);try {// 加锁防止并发更新if (lock.tryLock(100, 5000, TimeUnit.MILLISECONDS)) {Integer newStock = seckillMapper.selectStock(itemId);StockCache cache = new StockCache();cache.setStock(newStock);cache.setExpireTime(System.currentTimeMillis() + 3600 * 1000);redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(cache));}} finally {if (lock.isHeldByCurrentThread()) {lock.unlock();}}}
}

时序图

正常请求(未过期):
[用户] → 查缓存 → 命中(未过期)→ 返回结果过期请求(异步更新):
[用户] → 查缓存 → 命中(已过期)→ 返回旧数据↓异步线程更新缓存(加锁)

实战效果:缓存过期时无请求穿透至DB,MySQL查询量稳定在50QPS以内,超卖问题彻底解决。

方案2:分布式锁“串行化”查询

核心逻辑:热点key过期时,通过分布式锁保证只有一个线程能查询DB并更新缓存,其他线程等待重试。
适用场景:数据实时性要求高,无法接受旧数据。

实战代码(Redisson实现)

@Service
public class HotItemService {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate RedissonClient redissonClient;@Autowiredprivate ItemMapper itemMapper;/*** 查询热点商品详情(分布式锁防击穿)*/public ItemDTO getHotItem(Long itemId) {String cacheKey = "item:hot:" + itemId;// 1. 查询缓存String cacheVal = redisTemplate.opsForValue().get(cacheKey);if (cacheVal != null) {return JSON.parseObject(cacheVal, ItemDTO.class);}// 2. 缓存未命中:加分布式锁RLock lock = redissonClient.getLock("lock:item:hot:" + itemId);try {// 最多等待500ms,持有锁3秒if (lock.tryLock(500, 3000, TimeUnit.MILLISECONDS)) {// 双重检查:防止锁等待期间已更新缓存cacheVal = redisTemplate.opsForValue().get(cacheKey);if (cacheVal != null) {return JSON.parseObject(cacheVal, ItemDTO.class);}// 3. 查询DB并更新缓存(设置过期时间30分钟)ItemDTO item = itemMapper.selectById(itemId);redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(item), 30, TimeUnit.MINUTES);return item;} else {// 4. 获取锁失败:重试(最多3次)for (int i = 0; i < 3; i++) {Thread.sleep(50); // 短暂等待cacheVal = redisTemplate.opsForValue().get(cacheKey);if (cacheVal != null) {return JSON.parseObject(cacheVal, ItemDTO.class);}}// 重试失败:返回DB结果(兜底)return itemMapper.selectById(itemId);}} finally {if (lock.isHeldByCurrentThread()) {lock.unlock();}}}
}

实战效果:热点key过期时,仅1个线程查询DB,其他线程从缓存获取,MySQL峰值QPS从2000降至5,接口响应时间从500ms降至50ms。

方案3:熔断降级(极端情况保护)

核心逻辑:当DB压力过大时,通过熔断组件(如Resilience4j)临时返回缓存旧值或默认值,避免DB被压垮。

实战代码(Resilience4j配置)

@Configuration
public class CircuitBreakerConfig {@Beanpublic CircuitBreakerRegistry circuitBreakerRegistry() {CircuitBreakerConfig config = CircuitBreakerConfig.custom().failureRateThreshold(50) // 失败率超50%触发熔断.waitDurationInOpenState(Duration.ofSeconds(10)) // 熔断10秒.permittedNumberOfCallsInHalfOpenState(5) // 半开状态允许5次调用.slidingWindowSize(100) // 滑动窗口大小100.build();return CircuitBreakerRegistry.of(config);}
}@Service
public class ItemService {@Autowiredprivate CircuitBreakerRegistry circuitBreakerRegistry;@Autowiredprivate ItemMapper itemMapper;@Autowiredprivate StringRedisTemplate redisTemplate;/*** 带熔断的DB查询(兜底方案)*/public ItemDTO queryFromDBWithFallback(Long itemId) {CircuitBreaker breaker = circuitBreakerRegistry.circuitBreaker("itemDBQuery");// 包装DB查询方法,配置熔断降级return Try.ofSupplier(CircuitBreaker.decorateSupplier(breaker, () -> itemMapper.selectById(itemId))).recover(Exception.class, e -> {log.warn("DB查询熔断,使用缓存旧值,itemId={}", itemId, e);// 熔断时返回缓存旧值(即使过期)String oldVal = redisTemplate.opsForValue().get("item:hot:" + itemId);return oldVal != null ? JSON.parseObject(oldVal, ItemDTO.class) : buildDefaultItem(itemId);}).get();}// 构建默认商品(极端降级)private ItemDTO buildDefaultItem(Long itemId) {ItemDTO defaultItem = new ItemDTO();defaultItem.setId(itemId);defaultItem.setName("商品信息加载中");return defaultItem;}
}

实战效果:DB压力过大时自动熔断,返回缓存旧值,接口成功率保持99.9%,无服务雪崩。

击穿防御总结

方案适用场景优点缺点实施成本
逻辑永不过期实时性要求不高无并发穿透,性能好可能返回旧数据
分布式锁实时性要求高数据一致,实现简单锁竞争可能导致延迟
熔断降级极端流量保护兜底保障,防止DB雪崩影响用户体验

文章转载自:

http://kOCNuCGB.Lstmg.cn
http://eKKdr7Da.Lstmg.cn
http://rdgSErdY.Lstmg.cn
http://VsgiQR8m.Lstmg.cn
http://ys4o6ARw.Lstmg.cn
http://V6GXJpBZ.Lstmg.cn
http://uL1dC7hV.Lstmg.cn
http://X7apijLJ.Lstmg.cn
http://AgRPeumX.Lstmg.cn
http://tSEHRGFL.Lstmg.cn
http://C1dkp5Tm.Lstmg.cn
http://LmSluis4.Lstmg.cn
http://TUW7Et2y.Lstmg.cn
http://XoWLMz1e.Lstmg.cn
http://cVdSYXEb.Lstmg.cn
http://i9YTrXre.Lstmg.cn
http://xgwezP19.Lstmg.cn
http://fBAqJViE.Lstmg.cn
http://WNdPskST.Lstmg.cn
http://KK9hTcQc.Lstmg.cn
http://CCV8Fd65.Lstmg.cn
http://q7UZjUG4.Lstmg.cn
http://7su2oTxr.Lstmg.cn
http://iMJbBBbr.Lstmg.cn
http://wvx3AQ9X.Lstmg.cn
http://JDchRdu6.Lstmg.cn
http://3fzIr8MR.Lstmg.cn
http://bk4JcnAS.Lstmg.cn
http://I3mBo1tL.Lstmg.cn
http://2fvqxVzP.Lstmg.cn
http://www.dtcms.com/a/380117.html

相关文章:

  • MongoDB BI Connector 详细介绍与使用指南(手动安装方式,CentOS 7 + MongoDB 5.0.5)
  • 【计算机网络】HTTP协议(一)——超文本传输协议
  • 【国内电子数据取证厂商龙信科技】被格式化的手机如何恢复数据
  • 【项目】 :C++ - 仿mudou库one thread one loop式并发服务器实现(模块划分)
  • 采集集群外的k8s(prometheus监控)
  • AI 玩转网页自动化无压力:基于函数计算 FC 构建 Browser Tool Sandbox
  • Redisson原理与面试问题解析
  • ICCV 2025 | 首次引入Flash Attention,轻量SR窗口扩至32×32还不卡!
  • 关于线性子空间(Linear Subspace)的数学定义
  • OpenHarmony AVSession深度解析(二):从本地会话到分布式跨设备协同的完整生命周期管理
  • 12.NModbus4在C#上的部署与使用 C#例子 WPF例子
  • 迅为RK3568开发板Linux_NVR_SDK 系统开发-扩展根文件系统
  • OpenCV:特征提取
  • Zynq开发实践(FPGA之第一个vivado工程)
  • 数字人技术如何与数字孪生深度融合?
  • 如何生成 GitHub Token(用于 Hexo 部署):保姆级教程+避坑指南
  • Python uv常用命令及使用详解
  • MySQL主从同步参数调优案例
  • Python的uv包管理工具使用
  • 构建python3.11+uv+openssh环境的docker镜像
  • RabbitMQ的核心使用示例
  • 大数据电商流量分析项目实战:Hive 数据仓库(三)
  • 【Kubernetes】Tomcat 启用 Prometheus 监控指标
  • 数字人分身 + 矩阵系统聚合的源码搭建与定制开发
  • 如何使用 OCR 提取扫描件 PDF 的文本(Python 实现)
  • 并发:使用volatile和不可变性实现线程安全
  • 【qml入门】在qml项目上加入用户登录qml页面(包含源码)
  • 通义灵码产品演示: 数据库设计与数据分析
  • 大疆图传十公里原理:无人机图传技术解析
  • 【论文阅读】小模型是智能体的未来