问题背景
在一次线上故障中,某热门商品的缓存key恰好过期,导致大量请求直接击穿到数据库,引发系统性能问题。本文将详细分析处理过程和解决方案。
故障现象
1 2 3 4 5 6 7 8
| ┌──────────┐ ┌──────────┐ ┌──────────┐ │ 用户请求 │ ──────> │ Redis │ ──────> │ 数据库 │ └──────────┘ └──────────┘ └──────────┘ │ × !!! │ 缓存失效 QPS暴增 │ 响应变慢 └─────────────────────────────────────────┘ 大量请求直接访问数据库
|
主要表现:
- Redis某个key突然失效
- 大量并发请求涌入数据库
- 数据库CPU使用率飙升
- 系统响应时间显著增加
紧急处理流程
1. 数据库限流保护
1 2 3 4 5 6 7 8 9 10 11 12
| @Slf4j public class DbProtector { private RateLimiter rateLimiter = RateLimiter.create(100.0);
public Product queryProduct(Long productId) { if (!rateLimiter.tryAcquire()) { log.warn("数据库访问被限流,productId: {}", productId); throw new RuntimeException("系统繁忙,请稍后重试"); } return productMapper.selectById(productId); } }
|
2. 问题商品下线
1 2 3 4 5 6 7
| ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 运维平台 │--->│ 配置中心 │--->│ 应用服务 │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ └──────────────────────────────────────┘ 更新商品状态为"已下线"
|
3. 手动Mock缓存
1 2 3 4 5 6 7 8 9 10 11 12
| @Service public class CacheRecoveryService { @Autowired private RedisTemplate redisTemplate; public void mockProductCache(Long productId, Product product) { String cacheKey = "product:" + productId; redisTemplate.opsForValue().set(cacheKey, product, 5, TimeUnit.MINUTES); log.info("Mock cache success for productId: {}", productId); } }
|
4. 重启服务
1 2 3 4 5 6 7 8 9
| 分批重启流程: ┌────────────┐ ┌────────────┐ ┌────────────┐ │ 实例1下线 │ --> │ 实例2下线 │ --> │ 实例3下线 │ └────────────┘ └────────────┘ └────────────┘ │ │ │ ▼ ▼ ▼ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ 实例1上线 │ --> │ 实例2上线 │ --> │ 实例3上线 │ └────────────┘ └────────────┘ └────────────┘
|
长期解决方案
1. 缓存预热
1 2 3 4 5 6 7 8 9 10 11
| @Component public class CacheWarmer { @Scheduled(cron = "0 0 3 * * ?") public void warmHotProducts() { List<Long> hotProductIds = getHotProductIds(); for (Long productId : hotProductIds) { Product product = productService.getById(productId); cacheService.setProductCache(productId, product); } } }
|
2. 双重检查锁防击穿
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public Product getProduct(Long productId) { String cacheKey = "product:" + productId; Product product = redisTemplate.opsForValue().get(cacheKey); if (product == null) { String lockKey = "lock:" + productId; try { if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS)) { product = redisTemplate.opsForValue().get(cacheKey); if (product == null) { product = productMapper.selectById(productId); redisTemplate.opsForValue().set(cacheKey, product, 1, TimeUnit.HOURS); } } } finally { redisTemplate.delete(lockKey); } } return product; }
|
3. 缓存降级方案
1 2 3 4 5 6 7 8 9
| 正常访问流程: ┌──────────┐ ┌──────────┐ ┌──────────┐ │ 请求 │ --> │ Redis │ --> │ 数据库 │ └──────────┘ └──────────┘ └──────────┘
降级后流程: ┌──────────┐ ┌──────────┐ ┌──────────┐ │ 请求 │ --> │ 本地缓存 │ --> │ 数据库 │ └──────────┘ └──────────┘ └──────────┘
|
监控预警
缓存监控指标:
告警规则:
1 2 3 4 5 6 7 8 9
| rules: - name: "缓存击穿告警" conditions: - metric: "cache.miss.rate" threshold: 80% - metric: "db.qps" threshold: 1000 duration: "1m" severity: "critical"
|
经验总结
预防措施:
应急处理:
长期规划: