Redis缓存策略实战:穿透、击穿、雪崩的三层防御体系搭建
Redis缓存是数据库高可用架构中的标配组件,但”用Redis”和”用好Redis”之间差了一整套防御体系的设计。缓存穿透、击穿、雪崩这三个问题不是理论上的可能,而是每个高流量系统在生产环境中都会遇到的真实故障。这篇文章从防御体系的角度,讲清楚每层防御的原理、实现和适用边界。
缓存穿透:查询不存在的数据,请求直达数据库
穿透场景:大量请求查询一条不存在的数据(比如ID=-1的商品),Redis没有缓存,每次都打到数据库。攻击场景下这个问题尤其严重。
第一层防御:布隆过滤器
把所有可能被查询的合法key放入布隆过滤器,请求先过布隆过滤器,不存在的直接拒绝:
# Redis布隆过滤器实现(需要RedisBloom模块)
# 添加元素
BF.ADD valid_product_ids 10001
BF.ADD valid_product_ids 10002
# 批量添加
BF.MADD valid_product_ids 10003 10004 10005
# 检查元素是否存在
BF.EXISTS valid_product_ids 10001 # 返回1
BF.EXISTS valid_product_ids 99999 # 返回0
// Java应用层布隆过滤器(Guava)
BloomFilter<Long> productFilter = BloomFilter.create(
Funnels.longFunnel(),
1000000, // 预期元素数量
0.01 // 误判率1%
);
// 启动时加载所有合法ID
productRepo.findAllIds().forEach(productFilter::put);
// 查询前先过滤
public Product getProduct(long id) {
if (!productFilter.mightContain(id)) {
return null; // 布隆过滤器说不存在,直接返回
}
// 布隆过滤器说可能存在,走正常查询流程
return queryWithCache(id);
}
布隆过滤器的局限:有误判率(说存在但实际不存在),且不能删除元素。适合key集合相对稳定的场景。
第二层防御:空值缓存
对于布隆过滤器误判或动态数据集无法预加载的场景,缓存空值是兜底方案:
def get_product(product_id):
cache_key = f"product:{product_id}"
# 先查缓存
cached = redis.get(cache_key)
if cached is not None:
if cached == "NULL":
return None # 空值缓存命中
return json.loads(cached)
# 缓存未命中,查数据库
product = db.query("SELECT * FROM products WHERE id = ?", product_id)
if product is None:
# 缓存空值,TTL设置较短(防止数据后续被创建)
redis.setex(cache_key, 60, "NULL")
else:
# 正常缓存
redis.setex(cache_key, 3600, json.dumps(product))
return product
空值缓存的TTL必须短(30-120秒),否则合法数据被创建后用户看不到。对攻击场景,短TTL的空值缓存已经足够挡住绝大部分流量。
缓存击穿:热点key过期瞬间,大量请求涌入数据库
击穿场景:某个热点key(比如秒杀商品详情)过期的那一秒,成千上万并发请求同时发现缓存不存在,全部打到数据库。
防御一:逻辑过期(不设TTL,数据中嵌入过期时间)
// 逻辑过期方案:Redis中永不过期,过期时间写在数据里
public Product getProductWithLogicalExpire(Long id) {
String cacheKey = "product:" + id;
String json = redis.get(cacheKey);
if (json == null) {
// 真正不存在,查数据库并缓存
return queryAndCache(id);
}
ProductCacheData data = JSON.parseObject(json, ProductCacheData.class);
if (data.getExpireTime().isBefore(LocalDateTime.now())) {
// 逻辑过期——只允许一个线程重建缓存
String lockKey = "lock:" + cacheKey;
if (redis.setnx(lockKey, "1", 10, SECONDS)) {
try {
// 获取锁成功,查数据库更新缓存
Product product = db.queryProduct(id);
data.setData(product);
data.setExpireTime(LocalDateTime.now().plusHours(1));
redis.set(cacheKey, JSON.toJSONString(data));
} finally {
redis.del(lockKey);
}
}
// 其他线程返回旧数据(不阻塞)
}
return data.getData();
}
逻辑过期的优势:永远不会出现所有线程同时穿透到数据库的情况。获取锁失败的线程返回旧数据,对大多数业务场景可接受。代价是数据有一小段时间的不一致窗口。
防御二:互斥锁重建
对一致性要求更高的场景,获取不到锁的线程等待而不是返回旧数据:
def get_product_with_mutex(product_id):
cache_key = f"product:{product_id}"
lock_key = f"lock:{cache_key}"
while True:
cached = redis.get(cache_key)
if cached:
return json.loads(cached)
# 尝试获取重建锁
if redis.set(lock_key, "1", nx=True, ex=5):
try:
product = db.query_product(product_id)
redis.setex(cache_key, 3600, json.dumps(product))
return product
finally:
redis.delete(lock_key)
else:
# 等待50ms后重试
time.sleep(0.05)
缓存雪崩:大量key同时过期或Redis节点宕机
雪崩场景:批量缓存key设置了相同TTL,到期时全部同时过期;或者Redis主节点宕机导致大面积缓存失效。
防御一:TTL随机偏移
# 设置缓存时,TTL加随机偏移防止同时过期
import random
def set_cache_with_jitter(key, value, base_ttl=3600):
jitter = random.randint(-300, 300) # ±5分钟偏移
ttl = max(60, base_ttl + jitter) # 下限保护
redis.setex(key, ttl, value)
防御二:多级缓存架构
# L1: 本地缓存(JVM/Caffeine,进程级)
# L2: Redis集群(分布式缓存)
# L3: 数据库
def get_product_multi_level(product_id):
# L1本地缓存
local_cached = local_cache.get(f"product:{product_id}")
if local_cached:
return local_cached
# L2 Redis
redis_cached = redis.get(f"product:{product_id}")
if redis_cached:
local_cache.put(f"product:{product_id}", redis_cached, 300) # 本地5分钟
return json.loads(redis_cached)
# L3 数据库
product = db.query_product(product_id)
if product:
redis.setex(f"product:{product_id}", 3600, json.dumps(product))
local_cache.put(f"product:{product_id}", product, 300)
return product
多级缓存的核心价值:当Redis整体不可用时,本地缓存还能扛住一段时间,给Redis恢复争取窗口。本地缓存的容量有限,只缓存热点数据,TTL要短(5分钟以内),防止本地缓存和Redis数据长时间不一致。
Redis缓存策略的监控指标与告警阈值
防御体系搭建完,还需要持续监控验证有效性:
# Redis关键监控指标
redis-cli info stats | grep -E "keyspace_hits|keyspace_misses"
# 命中率 = hits / (hits + misses),低于95%需要排查
# 监控缓存穿透率
redis-cli info stats | grep "keyspace_misses"
# 监控Redis内存使用
redis-cli info memory | grep "used_memory_human"
# 监控慢查询
redis-cli slowlog get 10
告警规则建议:
- 缓存命中率持续5分钟低于90% → 告警,检查穿透或过期策略
- Redis内存使用超过maxmemory的85% → 告警,需要扩容或淘汰策略调整
- 慢查询超过10ms → 告警,检查大key或复杂命令
数据库运维中,Redis缓存策略不是独立的技术选型,而是和MySQL慢查询诊断、数据备份恢复策略一起构成整体的数据访问层防御。缓存解决的是读压力,分库分表方案解决的是存储压力,两者配合才能应对真正的流量洪峰。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/redis-huan-cun-ce-lyue-shi-zhan-chuan-tou-ji-chuan-xue-beng/