欢迎光临
我们一直在努力

Redis篇(十五):缓存雪崩、击穿、穿透

一、缓存三大问题全景

在高并发场景下,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 过期 控制并发重建,避免重复查询 互斥锁 + 逻辑不过期 + 热点预热
穿透 查询不存在数据 拦截非法请求,减少无效访问 布隆过滤器 + 空值缓存 + 参数校验

如果本文对你有帮助,欢迎 点赞 👍 + 收藏 ⭐ + 关注 🔖,你的支持是我持续创作的动力!

赞(0)
未经允许不得转载:171主机测评 » Redis篇(十五):缓存雪崩、击穿、穿透
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址