引入Redis缓存后数据库依然被打挂,通常是穿透、击穿、雪崩三类问题没防护到位。这三类故障的现象相似(数据库负载突增、接口超时),成因和对策完全不同。这篇文章把三者的判定方法与工程实现讲透,方案基于Redis与常见业务架构,代码以Python为例。
三种缓存故障的判定方法
先分清问题类型再动手:
缓存穿透:请求的数据在缓存和数据库里都不存在,每次都打到数据库。典型场景是恶意请求不存在的ID(id=-1)或扫描接口。特征是数据库QPS上升,但Redis命中率几乎不变,慢查询日志里全是按不存在主键的查询。
缓存击穿:某个热点key过期瞬间,大量并发同时回源数据库重建缓存。特征是监控上某个具体key的访问量巨大,过期时间点数据库出现脉冲式流量。
缓存雪崩:大批key同一时间集中过期,或Redis实例整体不可用,请求全部落到数据库。特征是数据库负载大面积同时飙升,且时间点与批量缓存写入或Redis故障时间吻合。
缓存穿透防护:布隆过滤器与空值缓存
两层防护组合使用。空值缓存处理零星的无效请求,不存在的数据也缓存一个短TTL的占位值:
import json
from redis import Redis
rdb = Redis(decode_responses=True)
def get_user(user_id: int):
key = f"user:{user_id}"
val = rdb.get(key)
if val is not None:
if val == "__NULL__":
return None # 空值缓存,直接返回,不查库
return json.loads(val)
user = db.query_user(user_id)
if user is None:
# 不存在的数据缓存60秒,防止反复穿透
rdb.set(key, "__NULL__", ex=60)
return None
rdb.set(key, json.dumps(user), ex=3600)
return user
布隆过滤器处理海量ID校验,把全量合法ID预加载,请求先过过滤器,不存在的直接拒绝。Redis方案可用RedisBloom模块:
# RedisBloom方式,误判率0.1%、容量1亿
rdb.execute_command("BF.RESERVE", "user:bloom", 0.001, 100000000)
# 启动时全量灌入合法ID
for uid in iter_all_user_ids():
rdb.execute_command("BF.ADD", "user:bloom", uid)
def check_user_exists(user_id: int) -> bool:
return rdb.execute_command("BF.EXISTS", "user:bloom", user_id) == 1
注意布隆过滤器有误判率(把不存在的判为存在),误判的流量会落到空值缓存层,两者正好互补;过滤器支持的ID集合新增没问题,删除需要换计数布隆或定期重建。
缓存击穿防护:互斥重建与逻辑过期
热点key过期的瞬间,只允许一个请求回源重建,其余请求等待或返回旧值。互斥重建用Redis分布式锁:
import time, uuid
def get_hot_detail(key: str, loader):
val = rdb.get(key)
if val is not None:
return json.loads(val)
lock_key = f"lock:{key}"
token = uuid.uuid4().hex
# 只有拿到锁的请求回源,其他请求短暂等待后重试读缓存
if rdb.set(lock_key, token, nx=True, ex=10):
try:
data = loader()
rdb.set(key, json.dumps(data), ex=1800)
return data
finally:
# 释放锁校验令牌,防止误删他人锁
lua = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) end"
rdb.eval(lua, 1, lock_key, token)
else:
time.sleep(0.05)
val = rdb.get(key)
return json.loads(val) if val else loader()
对可用性要求更高的场景用逻辑过期:缓存物理上永不过期,value里带过期时间字段,发现逻辑过期后由一个异步线程重建,所有请求立刻返回旧值。代价是短暂的数据不一致,换来零阻塞。
缓存雪崩防护:过期打散与多级缓存
过期时间打散是第一原则,在基础TTL上叠加随机偏移:
import random
def cache_with_jitter(key: str, data: dict, base_ttl: int = 3600):
ttl = base_ttl + random.randint(0, 600) # 1小时±10分钟随机
rdb.set(key, json.dumps(data), ex=ttl)
Redis高可用用主从加哨兵或Cluster集群,避免单点。业务侧再加一层本地缓存(进程内存,Python可用cachetools)构成多级缓存,Redis抖动时本地缓存兜底住最热的数据:
from cachetools import TTLCache
local = TTLCache(maxsize=10000, ttl=60) # 本地缓存60秒
def get_config(key: str):
if key in local:
return local[key]
val = rdb.get(f"cfg:{key}")
if val is None:
val = db.query_config(key)
if val is not None:
rdb.set(f"cfg:{key}", json.dumps(val), ex=300 + random.randint(0, 60))
else:
val = json.loads(val)
if val is not None:
local[key] = val
return val
本地缓存要控制容量与TTL,只放高频小对象,避免内存膨胀与数据滞后放大。
兜底机制:限流降级与熔断
缓存防御做完仍需最后防线:回源数据库的调用路径加信号量限流,数据库压力超阈值时熔断非核心查询直接返回默认值;核心接口准备降级开关。监控上盯三个指标:缓存命中率(健康值通常高于95%)、Redis连接与延迟、数据库QPS突增告警。命中率突然下滑是最灵敏的预警信号,往往比数据库告警早几分钟。
上线检查清单
缓存方案上线前逐项核对:全量key的TTL带随机偏移;无效ID请求被布隆过滤器或空值缓存拦截;热点key有过期刷新机制(定时刷新或逻辑过期);Redis为主从加哨兵或Cluster;数据库访问路径有限流熔断;命中率与数据库QPS有监控告警。三层防御(过滤与空值、互斥与打散、多级与熔断)各自独立生效,任何一层失效都不至于让数据库直接暴露在全量流量之下,这是缓存架构高可用的底线。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/redis-huan-cun-chuan-tou-ji-chuan-xue-beng-jie-jue-fang-an/