Redis作为高并发场景的核心缓存层,缓存穿透、雪崩、击穿是数据库运维中最频发的问题类型。三者表象相似但成因不同,诊断思路和解决方案存在本质差异。本文从问题现象出发,给出每种问题的诊断方法、代码实现和架构层面的预防措施。
缓存穿透:不存在的Key穿透到数据库
缓存穿透指查询一个数据库中不存在的数据,由于缓存无法命中(缓存中不存在该Key),每次请求都打到数据库。恶意攻击或爬虫大量请求不存在的ID时,数据库连接池迅速耗尽。诊断特征:Redis miss率异常升高,数据库QPS突增,但返回结果为空。
解决方案一:缓存空值。查询数据库未命中时,将空结果缓存到Redis并设置较短的过期时间(30秒至5分钟),避免同一不存在的Key反复穿透:
// Java实现:缓存空值防穿透
public User getUserById(Long userId) {
String cacheKey = "user:" + userId;
String cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
if ("NULL".equals(cached)) {
return null; // 空值缓存命中,直接返回
}
return JSON.parseObject(cached, User.class);
}
// 缓存未命中,查询数据库
User user = userMapper.selectById(userId);
if (user == null) {
// 缓存空值,过期时间较短
redisTemplate.opsForValue().set(cacheKey, "NULL", 30, TimeUnit.SECONDS);
return null;
}
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(user), 30, TimeUnit.MINUTES);
return user;
}
解决方案二:布隆过滤器。在缓存之前增加布隆过滤器层,请求先经过布隆过滤器判断Key是否存在,不存在则直接拒绝。布隆过滤器存在误判率(假阳性),但不会漏判(假阴性),适合数据集相对固定的场景:
// 使用Redisson布隆过滤器
@Configuration
public class BloomFilterConfig {
@Bean
public RBloomFilter<Long> userBloomFilter(RedissonClient redisson) {
RBloomFilter<Long> filter = redisson.getBloomFilter("user:bloom");
// 预计元素100万,误判率0.01%
filter.tryInit(1_000_000L, 0.0001);
// 启动时加载所有用户ID
List<Long> userIds = userMapper.selectAllIds();
userIds.forEach(filter::add);
return filter;
}
}
public User getUserWithBloom(Long userId) {
// 布隆过滤器前置拦截
if (!userBloomFilter.contains(userId)) {
return null; // 一定不存在,直接返回
}
// 继续正常缓存查询逻辑
return getUserById(userId);
}
缓存雪崩:大量Key同时过期导致数据库过载
缓存雪崩指大量Key在同一时间段过期,或Redis节点宕机导致缓存大面积失效,所有请求涌向数据库。与穿透的区别在于:穿透是单个不存在的Key反复查询,雪崩是大量存在的Key同时失效。诊断特征:数据库连接数飙升、CPU满载、响应时间剧增,且集中在某个时间点爆发。
核心解决方案:过期时间随机化。设置缓存过期时间时叠加随机偏移量,避免大量Key同时过期:
// 过期时间随机化防雪崩
public void cacheUser(User user) {
String cacheKey = "user:" + user.getId();
int baseExpire = 1800; // 基础过期时间30分钟
int randomOffset = new Random().nextInt(300); // 随机0到5分钟
int expireTime = baseExpire + randomOffset;
redisTemplate.opsForValue().set(
cacheKey,
JSON.toJSONString(user),
expireTime, TimeUnit.SECONDS
);
}
// 多级缓存架构防雪崩
public User getUserMultiLevel(Long userId) {
// L1: 本地缓存(Caffeine)
User user = caffeineCache.getIfPresent(userId);
if (user != null) return user;
// L2: Redis分布式缓存
String cached = redisTemplate.opsForValue().get("user:" + userId);
if (cached != null) {
user = JSON.parseObject(cached, User.class);
caffeineCache.put(userId, user); // 回填L1
return user;
}
// L3: 数据库,使用分布式锁防止缓存重建并发
String lockKey = "lock:user:" + userId;
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
// 双重检查
cached = redisTemplate.opsForValue().get("user:" + userId);
if (cached != null) {
return JSON.parseObject(cached, User.class);
}
user = userMapper.selectById(userId);
if (user != null) {
cacheUser(user);
caffeineCache.put(userId, user);
}
return user;
}
// 获取锁失败,短暂等待后重试读取缓存
Thread.sleep(50);
return getUserMultiLevel(userId);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
缓存击穿:热点Key过期瞬间并发穿透
缓存击穿指某个热点Key过期的瞬间,大量并发请求同时查询该Key,全部穿透到数据库。与雪崩的区别在于:雪崩是大量Key同时失效,击穿是单个热点Key失效。典型场景:秒杀商品详情、热门文章。诊断特征:单个数据库查询QPS突增,对应一个特定的缓存Key。
解决方案:互斥锁重建缓存。缓存未命中时,只有一个线程查询数据库并重建缓存,其他线程等待或返回旧数据:
// 互斥锁防击穿
public Product getHotProduct(Long productId) {
String cacheKey = "product:hot:" + productId;
String cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return JSON.parseObject(cached, Product.class);
}
// 缓存未命中,获取分布式锁
String lockKey = "lock:product:" + productId;
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
// 双重检查,避免重复查库
cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return JSON.parseObject(cached, Product.class);
}
Product product = productMapper.selectById(productId);
if (product != null) {
// 逻辑过期:不设置TTL,通过value中的过期时间字段判断
redisTemplate.opsForValue().set(
cacheKey, JSON.toJSONString(product), 2, TimeUnit.HOURS
);
}
return product;
} finally {
redisTemplate.delete(lockKey);
}
} else {
// 未获取到锁,短暂自旋等待缓存重建
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
return getHotProduct(productId);
}
}
架构层面预防措施:热点Key永不过期(逻辑过期),在value中嵌入过期时间戳,后台异步线程定期刷新;Redis Cluster部署保障高可用,主节点宕机后Sentinel自动故障转移;数据库连接池设置合理上限(如HikariCP maximumPoolSize=20),防止缓存失效时数据库被压垮。监控告警层面,通过Redis INFO命令采集expired_keys指标,配合Grafana监控缓存过期速率,异常波动及时预警。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/redis-huan-cun-chuan-tou-xue-beng-ji-chuan-wen-ti-zhen-duan/