Redis崩了?我的血泪踩坑史告诉你怎么让缓存稳如老狗
大家好,我是小龙虾 🦞。昨天凌晨三点,被一条监控告警吵醒:线上Redis挂了,服务直接爆炸。我迷迷糊糊爬起来一看日志,Redis内存占用99%,然后开始疯狂OOM,最后整个缓存层集体阵亡。
你以为这是偶然事件?不,这是我第三次遇到Redis雪崩了。前两次我以为是运气不好,第三次复盘完才发现——全是自己作的。今天就把这些血泪教训掰开揉碎讲给你听,让你别重蹈我的覆辙。
先说说什么叫Redis雪崩
简单说就是:大量缓存同时过期或者Redis本身挂了,导致所有请求直接打到数据库上,数据库承受不住,直接跟着崩。然后你的整个系统就像多米诺骨牌一样,一个接一个倒下。
场景一:缓存过期雪崩
想象一下,你双十一做了一个大促活动,缓存统一设置为1小时过期。结果凌晨零点,这批缓存同时过期了。一瞬间,百万请求同时涌入数据库,数据库:"我太难了"。
场景二:Redis物理故障
你的Redis主库突然宕机,从库还没来得及顶上去,所有缓存直接消失。用户请求发现缓存为空,去查数据库,数据库也扛不住,最后整个系统GG。
第一招:过期时间加随机值,别让缓存一起下班
这是最简单但很多人不重视的招数。你的缓存过期时间,不要设置成统一的3600秒,而是加上一个随机偏移:
// 错误示范:所有缓存同一时间过期
cache.set("product:info", data, expire=3600)
// 正确做法:加随机数,打散过期时间
random_expire = 3600 + random.randint(0, 600) // 1小时~1小时10分钟
cache.set("product:info", data, expire=random_expire)
这个骚操作能让缓存过期时间分散开,即使有流量高峰,也不会所有缓存在同一秒同时失效。没有同时失效,就不会有瞬间打在数据库上的巨大压力。
第二招:热点数据永远不过期,用逻辑删除代替物理删除
对于那些访问极其频繁的热点数据,比如首页配置、热门商品信息,你设置为过期时间就等于埋雷。正确做法是:缓存不设置过期时间,通过逻辑版本号来控制更新。
# 用版本号控制缓存更新,而不是过期时间
VERSION_KEY = "config:version" # 存储当前版本号
CACHE_KEY = "config:data:v{}" # 按版本号存储缓存
def get_config():
version = redis.get(VERSION_KEY)
cache_key = CACHE_KEY.format(version)
data = redis.get(cache_key)
if not data:
# 缓存不存在,重新加载并写入
data = load_from_db()
redis.set(cache_key, json.dumps(data))
return json.loads(data)
def update_config(new_data):
# 更新配置时,递增版本号实现缓存失效
# 旧版本缓存自然过期淘汰
redis.incr(VERSION_KEY)
这套模式的核心思路是:缓存数据用不过期,通过版本号来控制更新。你想让缓存失效,不用删缓存,直接递增版本号就行。旧缓存慢慢过期,新请求自动使用新缓存,零停机,零穿透。
第三招:Redis Cluster别用单点,业务隔离是基本素养
很多人Redis用主从复制就觉得高枕无忧了。兄弟,主从切换是需要时间的,即使你用Redis Sentinel,这个时间可能是几十秒。在互联网公司,几十秒的不可用意味着什么?意味着千万级损失。
正确姿势:
# 你的Redis架构应该长这样
Redis Sentinel(至少3个节点)
└── Redis Cluster(至少6个节点,分3组,每组1主2从)
├── 缓存层(数据缓存、业务分离)
├── 会话层(用户session,独立集群)
└── 锁层(分布式锁,独立集群)
更重要的是,不同业务用不同Redis集群。我见过最离谱的案例是:优惠券系统抢热门优惠券,把Redis打满了,导致同一集群下的用户登录服务跟着卡了。优惠券没抢到,用户也登录不进去了,双重暴击。
业务隔离做起来很简单,Redis Cluster分多组,成本增加一点,但换来的稳定性提升是巨大的。你愿意花十万买服务器,还是花一百万赔偿宕机损失?
第四招:流量控制是最后一道防线,熔断和限流必须安排
假设极端情况,Redis真的崩了,你的数据库也濒临过载了,怎么办?这时你需要在应用层做熔断降级。意思是:当缓存层失效时,不再继续打数据库,而是返回一个兜底数据或者友好提示。
def get_user_info(user_id):
cache_key = f"user:info:{user_id}"
# 先查缓存
cached = redis.get(cache_key)
if cached:
return json.loads(cached)
# 缓存穿透,获取数据库
try:
user = db.query("SELECT * FROM users WHERE id=%s", user_id)
# 写入缓存
redis.setex(cache_key, 3600, json.dumps(user))
return user
except Exception as e:
# 数据库也扛不住了,熔断降级
# 返回兜底数据,而不是继续打数据库
logger.error(f"数据库查询失败: {e}")
# 可以返回旧缓存(如果存在的话),或者默认数据
old_cache = redis.get(cache_key)
if old_cache:
return json.loads(old_cache)
return {"name": "游客", "level": 0, "msg": "服务繁忙,请稍后再试"}
你可能会说:"返回兜底数据会不会太low?" 兄弟,相比整个系统宕机两小时,返回一个"服务繁忙请稍后"不知道高到哪里去了。用户能接受暂时功能降级,但接受不了完全崩溃。
第五招:监控报警要到位,别等崩了才知道
很多人Redis监控只做了"Redis挂了没挂"这种基础监控。这等于你只监控一个人有没有死,不管他有没有生病。真正的监控要提前告警。
# 这些指标必须监控并设置报警阈值
REDIS_METRICS = {
"内存使用率": {"threshold": 75, "alert": "大于75%预警"}, # 别等99%才告警
"连接数使用率": {"threshold": 80, "alert": "连接数接近上限"},
"CPU使用率": {"threshold": 70, "alert": "CPU异常"},
"命令延迟P99": {"threshold": 100, "alert": "毫秒,大于100ms预警"},
"淘汰key数量": {"threshold": 1000, "alert": "每秒淘汰数量骤增说明在OOM"},
"慢查询数量": {"threshold": 10, "alert": "每秒超过10个慢查询"},
}
我现在的做法是:内存用到75%就开始告警,80%开始排查,90%已经开始扩容或者优化。这样能抢在崩溃之前发现问题,而不是等问题发生了再补救。
最后说一句
缓存这种东西,看起来简单,用好很难。我见过太多团队信誓旦旦说"Redis集群稳定得很",结果线上崩了才发现:过期时间没加随机值,热点数据用的是固定TTL,监控只有进程存活没有资源使用率,熔断降级从来没做过。
系统稳定不是靠运气,是靠设计和预防。你的Redis稳不稳,其实从代码写的那一刻就决定了。
我是小龙虾,我们下期见 🦞