一、缓存三大问题全景
在高并发场景下,Redis 缓存失效可能导致数据库被瞬间压垮。这三个问题虽然名字相似,但成因和解决方案完全不同。

1.1 缓存雪崩(Cache Avalanche)
问题本质:大量 key 在同一时间过期或 Redis 故障宕机,所有请求同时打到数据库,导致数据库瞬间被压垮。
触发条件:
- 批量设置缓存时使用了相同的 TTL
- Redis 集群宕机或重启
- 缓存服务故障
影响:
- 数据库压力骤增
- 服务可能崩溃
- 影响范围大
1.2 缓存击穿(Cache Breakdown)
问题本质:某个热点 key 恰好过期,高并发请求瞬间穿透到数据库。
触发条件:
- 秒杀商品详情页缓存过期
- 热点新闻缓存失效
影响:
- 单个热点数据失效
- 并发查 DB
- 数据库瞬间高负载
1.3 缓存穿透(Cache Penetration)
问题本质:查询一个不存在的数据,由于缓存中没有,请求直接打到数据库。攻击者大量构造不存在的 key 进行查询,数据库将承受巨大压力。
触发条件:
- 恶意攻击构造不存在的 key
- 业务逻辑缺陷查询已删除数据
影响:
- 恶意攻击构造
- 数据库压力激增
- 缓存形同虚设
二、缓存雪崩:多层防御体系

2.1 第一层:预防(事前)
1. 均匀设置过期时间
// 基础 TTL + 随机偏移,避免大量 key 同时过期
int baseTtl = 3600;
int randomOffset = ThreadLocalRandom.current().nextInt(0, 600);
redisTemplate.opsForValue().set(key, data, Duration.ofSeconds(baseTtl + randomOffset));
2. 热点数据预热
// 系统启动时加载热点数据到缓存
@PostConstruct
public void preheatCache() {
List<String> hotKeys = getHotKeys();
for (String key : hotKeys) {
String data = db.query(key);
redisTemplate.opsForValue().set(key, data, Duration.ofHours(1));
}
}
3. 双 key 策略
// 主 key 设置过期时间,备 key 永不过期
public String getData(String key) {
String data = redisTemplate.opsForValue().get("main:" + key);
if (data == null) {
data = redisTemplate.opsForValue().get("backup:" + key);
if (data != null) {
// 异步重建主 key
asyncRebuildMainKey(key);
}
}
return data;
}
2.2 第二层:保护(事中)
1. 互斥锁
public String getDataWithLock(String key) {
String data = redisTemplate.opsForValue().get(key);
if (data != null) return data;
String lockKey = "lock:" + key;
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", Duration.ofSeconds(10));
if (locked) {
try {
data = redisTemplate.opsForValue().get(key);
if (data == null) {
data = db.query(key);
redisTemplate.opsForValue().set(key, data, Duration.ofHours(1));
}
} finally {
redisTemplate.delete(lockKey);
}
} else {
Thread.sleep(100);
return getDataWithLock(key);
}
return data;
}
2. 后台更新线程
// 缓存永久有效,后台线程定期刷新
@Scheduled(fixedRate = 300000) // 每 5 分钟
public void refreshCache() {
List<String> keys = getAllCacheKeys();
for (String key : keys) {
String newData = db.query(key);
redisTemplate.opsForValue().set(key, newData);
}
}
3. 多级缓存
客户端请求
↓
本地缓存 (Caffeine) —— 第一层,QPS 最高
↓
Redis 缓存 —— 第二层,分布式共享
↓
数据库 —— 最后一层
2.3 第三层:兜底(事后)
1. 服务熔断
@SentinelResource(value = "getData", fallback = "getDataFallback")
public String getData(String key) {
return redisTemplate.opsForValue().get(key);
}
public String getDataFallback(String key) {
// Redis 故障时直接返回错误
return "{"status":"degraded","data":null}";
}
2. 限流降级
// 只放行少量请求到 DB
@RateLimiter(name = "dbQuery", rate = 100, limit = 1000)
public String queryDb(String key) {
return db.query(key);
}
3. 高可用集群
# 主从 + 哨兵,自动故障转移
redis-sentinel sentinel.conf
三、缓存击穿:两种核心方案

3.1 方案一:互斥锁(Mutex Lock)
public String getHotData(String key) {
String data = redisTemplate.opsForValue().get(key);
if (data != null) return data;
String lockKey = "lock:" + key;
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", Duration.ofSeconds(10));
if (locked) {
try {
// 双重检查
data = redisTemplate.opsForValue().get(key);
if (data == null) {
data = db.query(key);
redisTemplate.opsForValue().set(key, data, Duration.ofHours(1));
}
} finally {
// Lua 脚本释放锁(保证原子性)
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey), "1");
}
} else {
Thread.sleep(100);
return getHotData(key);
}
return data;
}
特点:
- 强一致性
- 可能有等待延迟
- 适合库存、余额等强一致性场景
3.2 方案二:逻辑不过期(Logical Expiry)
// 缓存不设物理 TTL,通过逻辑时间判断
public String getDataWithLogicExpiry(String key) {
String json = redisTemplate.opsForValue().get(key);
if (json == null) return null;
CacheData cacheData = JSON.parseObject(json, CacheData.class);
// 逻辑未过期,直接返回
if (cacheData.getExpireTime() > System.currentTimeMillis()) {
return cacheData.getData();
}
// 逻辑已过期,获取锁后开启独立线程重建
String lockKey = "lock:" + key;
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", Duration.ofSeconds(10));
if (locked) {
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
String newData = db.query(key);
CacheData newCache = new CacheData(newData,
System.currentTimeMillis() + Duration.ofHours(1).toMillis());
redisTemplate.opsForValue().set(key, JSON.toJSONString(newCache));
} finally {
redisTemplate.delete(lockKey);
}
});
}
// 返回旧数据(逻辑过期但物理未删除)
return cacheData.getData();
}
特点:
- 高可用性
- 短暂数据不一致
- 适合商品详情、配置等允许短暂不一致场景
3.3 方案选型建议
| 一致性 | 强一致性 | 短暂不一致 |
| 可用性 | 可能有等待延迟 | 高可用,无等待 |
| 实现复杂度 | 简单 | 较复杂 |
| 适用场景 | 库存、余额等强一致性 | 商品详情、配置等 |
四、缓存穿透:三层拦截策略
4.1 第一层:参数校验
public String getData(String key) {
// 参数校验,拦截非法请求
if (key == null || key.isEmpty() || key.length() > 100) {
return null; // 直接返回,不查询缓存和 DB
}
// 业务校验,如 ID 范围
if (!isValidId(key)) {
return null;
}
// 继续查询缓存…
}
4.2 第二层:空值缓存
String data = redisTemplate.opsForValue().get(key);
if (data == null) {
data = db.query(key);
if (data == null) {
// 缓存空值,防止重复查询 DB(设置较短 TTL)
redisTemplate.opsForValue().set(key, "", Duration.ofMinutes(5));
} else {
redisTemplate.opsForValue().set(key, data, Duration.ofHours(1));
}
}
注意事项:
- 空值 TTL 不宜过长(建议 1~5 分钟)
- 空值占用内存,需定期清理
- 业务变更后需及时清除空值缓存
4.3 第三层:布隆过滤器
# 使用 RedisBloom 模块
BF.RESERVE users 0.01 1000000 # 误判率 1%,预计 100 万元素
BF.ADD users user:1001
BF.ADD users user:1002
# 查询
BF.EXISTS users user:1003
# 返回 0 → 一定不存在,直接返回
# 返回 1 → 可能存在,继续查缓存/DB
Java 实现:
@Service
public class BloomFilterService {
@Autowired
private RedissonClient redissonClient;
private RBloomFilter<String> bloomFilter;
@PostConstruct
public void init() {
bloomFilter = redissonClient.getBloomFilter("userFilter");
bloomFilter.tryInit(1000000L, 0.01);
// 加载已有数据
List<String> userIds = userDao.getAllUserIds();
userIds.forEach(bloomFilter::add);
}
public boolean mightContain(String userId) {
return bloomFilter.contains(userId);
}
}
五、综合对比表
| 雪崩 | 大量 key 同时失效 | 分散失效时间,降低 DB 压力 | 随机 TTL + 多级缓存 + 熔断 |
| 击穿 | 热点 key 过期 | 控制并发重建,避免重复查询 | 互斥锁 + 逻辑不过期 + 热点预热 |
| 穿透 | 查询不存在数据 | 拦截非法请求,减少无效访问 | 布隆过滤器 + 空值缓存 + 参数校验 |
如果本文对你有帮助,欢迎 点赞 👍 + 收藏 ⭐ + 关注 🔖,你的支持是我持续创作的动力!



