← 返回工具首页
Redis缓存失效了?我翻车后总结的解决方案
Redis缓存失效了?我翻车后总结的解决方案
凌晨两点,刚准备睡觉,手机突然炸了——报警说系统超时率飙升。我心里咯噔一下,爬起来一看:数据库 CPU 打满,Redis 命中率从 99% 掉到了 12%。缓存几乎全失效了。
这不是我第一次被 Redis 坑,但这次是最惨的一次。痛定思痛,我把所有翻车原因和解决方案整理了一遍,写成这篇实战指南。不想让你重蹈我的覆辙。
🚨 先说翻车现场
那天到底发生了什么?简单还原一下:
- 系统正常,Redis 命中率 99%+,数据库稳稳当当
- 某个业务定时任务跑了,把一批热点 key 的 TTL 设成了 60 秒
- 这批 key 在业务逻辑里没有做续期机制,60 秒一到集体过期
- 大量请求同时击穿缓存打到数据库,CPU 打满
- 雪崩了
听起来很低级?但这事真实发生在生产环境,而且不止我一个人踩过。缓存失效的坑远比想象中多——key 过期、内存淘汰、连接断开、穿透击穿雪崩、分布式锁失效,个个都能让你半夜爬起来。
🔍 Redis缓存失效的常见原因
1. Key 过期,但没人管
Redis 的 key 过期后不会立即删除,采用惰性删除(访问时删除)和定期删除(抽样检查)相结合的方式。如果过期 key 长时间没人访问,就会一直占着内存,直到被内存淘汰策略清理。
redis-cli --latency-history -i 1
redis-cli INFO memory | grep used_memory_human
redis-cli INFO keyspace
redis-cli --scan --pattern "user:*" | head -100 | xargs -I {} redis-cli TTL {}
更坑的是,有时候你是用 SETEX 设的过期时间,但业务代码里有个 Bug——更新用户信息时用的是 SET 而不是 SETEX,把原来的过期时间覆盖了,导致这个 key 永远不过期,或者反过来,永远提前过期。这两种情况我都见过。
2. 内存淘汰策略不匹配
Redis 默认 maxmemory-policy 是 noeviction,意思是内存满了就拒绝写入,不淘汰任何 key。但很多人装完 Redis 根本没配这个,等内存快满了才发现写不进去了。
另一种情况:设置了淘汰策略但选错了策略。
redis-cli CONFIG GET maxmemory-policy
redis-cli CONFIG SET maxmemory-policy allkeys-lru
redis-cli CONFIG SET maxmemory "2gb"
如果你用的是 volatile-lru 但很多 key 没设过期时间,那淘汰策略等于没生效,系统会退化成 noeviction 的行为——内存满了就拒绝写入。这坑了我整整三天。
3. Redis 连接断开/重连风暴
网络抖动、服务重启、Redis 主从切换——这些都会导致连接断开。如果你的客户端没有做好连接池管理和自动重连,连接断开的一瞬间所有请求都会报错。
redis-cli INFO clients | grep connected_clients
redis-cli client list | grep cmd=command
用 Redis Cluster 或者 Codis 的时候,一个节点挂了,客户端如果没有实现自动重定向,就会对那个节点的所有请求全部失败。解决方案是客户端连接池配合 retry_on_timeout 和 retry_on_error 参数。
4. 缓存穿透 / 击穿 / 雪崩
这三个问题很多人搞混,我分开说。
缓存穿透:查询一个根本不存在的数据,缓存里没有,查数据库也查不到,每次都打到后端。攻击者就喜欢用这招把你的数据库打穿。
解决方案:对查询结果为空的情况也缓存起来,key=NULL,TTL 设短一点(比如 60 秒)。或者使用布隆过滤器判断数据是否存在。
缓存击穿:一个热点 key 突然过期,瞬间大量请求全部打到数据库。穿透是"都没有",击穿是"只有一个过期了但大家都在抢"。
import redis
r = redis.Redis(host='localhost', port=6379)
def get_data(key):
cache = r.get(key)
if cache:
return cache
lock_key = f"lock:{key}"
if r.set(lock_key, "1", nx=True, ex=10):
try:
data = db.query(key)
r.setex(key, 300, data)
return data
finally:
r.delete(lock_key)
else:
import time; time.sleep(0.1)
return r.get(key)
缓存雪崩:大量 key 在同一时间过期,或者 Redis 本身挂了,所有请求全部打到数据库。雪崩是击穿的放大版,一死一片。
import random
ttl = 3000 + random.randint(0, 600)
r.setex("user:10086", ttl, user_data)
5. 分布式锁失效
用 Redis 做分布式锁,看起来很简单:SET key value NX EX 30。但这坑深得很。
核心问题:锁的持有者和服务端时间不同步。如果你的业务时间比 Redis 快 30 秒,锁会提前释放;如果慢 30 秒,锁会延迟释放。两种情况都会导致锁失效。
更常见的问题是:锁续期没做。业务流程执行时间超过了锁的 TTL,锁自动释放了,其他进程拿到锁,两个进程同时操作同一条数据。
import redis, threading, time
class RedisLock:
def __init__(self, client, key, ttl=30):
self.client = client
self.key = key
self.ttl = ttl
self.value = f"thread-{threading.current_thread().ident}"
self.acquired = False
def acquire(self):
self.acquired = self.client.set(
self.key, self.value, nx=True, ex=self.ttl
)
if self.acquired:
self._start_auto_renew()
return self.acquired
def _start_auto_renew(self):
def renew():
while self.acquired:
time.sleep(self.ttl / 3)
if self.client.get(self.key) == self.value.encode():
self.client.expire(self.key, self.ttl)
t = threading.Thread(target=renew, daemon=True)
t.start()
def release(self):
script = """
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
"""
self.client.eval(script, 1, self.key, self.value)
self.acquired = False
lock = RedisLock(r, "order:10086", ttl=30)
if lock.acquire():
try:
process_order("10086")
finally:
lock.release()
如果你用的是 Redisson 这个库,它已经帮你实现了自动续期。如果你手写分布式锁,一定要自己加续期机制,否则迟早出事。
🛠️ 5种检测方法
发现问题比解决问题更重要。以下是我用下来最有效的 5 种检测方法:
方法1:INFO 命令查看命中率
redis-cli INFO stats | grep keyspace
redis-cli INFO commandstats
方法2:MONITOR 实时抓命令
redis-cli --intrinsic-latency 100
redis-cli monitor --short | head -50
redis-cli --scan | xargs -I {} redis-cli DEBUG SLEEP 0.1 "GET {}" | \
awk '{print $1}' | sort | uniq -c | sort -rn | head -10
方法3:SCAN + TTL 分析过期 key 分布
import redis, time
r = redis.Redis(decode_responses=True)
cursor = "0"
expired_keys = []
no_ttl_keys = []
while True:
cursor, keys = r.scan(cursor=cursor, count=500)
for key in keys:
ttl = r.ttl(key)
if ttl == -1:
no_ttl_keys.append(key)
elif ttl < 60:
expired_keys.append((key, ttl))
if cursor == "0":
break
print(f"即将过期的key(TTL<60s): {len(expired_keys)}")
print(f"没有过期时间的key: {len(no_ttl_keys)}")
方法4:Redis RDB 分析(持久化文件)
redis-cli --rdb /tmp/redis-dump.rdb
rdb --command memory /tmp/redis-dump.rdb | head -20
方法5:客户端埋点 + Prometheus 监控
import redis, time, prometheus_client
cache_hits = prometheus_client.Counter('cache_hits_total', 'Cache hits')
cache_misses = prometheus_client.Counter('cache_misses_total', 'Cache misses')
cache_latency = prometheus_client.Histogram('cache_latency_seconds', 'Cache latency')
def get_from_cache(key):
with cache_latency.time():
val = r.get(key)
if val:
cache_hits.inc()
return val
cache_misses.inc()
return None
🔧 解决方案汇总
1. LRU(Least Recently Used)— 最近最少使用
LRU 是最常见的淘汰策略,淘汰最近最少访问的数据。适合热点数据明显的场景。
redis-cli CONFIG SET maxmemory-policy allkeys-lru
redis-cli CONFIG SET maxmemory-samples 5
2. LFU(Least Frequently Used)— 最不经常使用
LRU 只看访问时间,LFU 看访问频率。适合数据访问频率差异大的场景。
redis-cli CONFIG SET maxmemory-policy volatile-lfu
3. TTL 主动更新
不要被动等 Redis 删除过期 key,在业务层主动续期。
def get_user(user_id):
cache_key = f"user:{user_id}"
data = r.get(cache_key)
if data:
ttl = r.ttl(cache_key)
if ttl < 300:
r.expire(cache_key, 3600)
return data
data = db.query(f"SELECT * FROM users WHERE id={user_id}")
r.setex(cache_key, 3600, json.dumps(data))
return data
4. 主动更新(Cache Aside 最佳实践)
def read_user(user_id):
cache_key = f"user:{user_id}"
data = r.get(cache_key)
if data:
return json.loads(data)
data = db.query(f"SELECT * FROM users WHERE id={user_id}")
r.setex(cache_key, 3600, json.dumps(data))
return data
def update_user(user_id, fields):
db.execute(f"UPDATE users SET ... WHERE id={user_id}")
r.delete(f"user:{user_id}")
5. 多级缓存 + 熔断降级
别把 Redis 当唯一的救命稻草,Redis 挂了怎么办?
import functools
local_cache = {}
def get_user(user_id):
cache_key = f"user:{user_id}"
if cache_key in local_cache:
return local_cache[cache_key]
try:
data = r.get(cache_key)
if data:
local_cache[cache_key] = data
return data
except redis.ConnectionError:
pass
data = db.query(f"SELECT * FROM users WHERE id={user_id}")
return data
🚨 监控告警配置
等出问题了再修是被动的,要在问题发生时就告警。以下是我生产环境在用的监控指标:
- 缓存命中率:低于 80% 告警
- 内存使用率:超过 maxmemory 的 85% 告警
- 连接数:突然变化(暴涨或归零)告警
- 命令延迟:P99 > 100ms 告警
- OOM:Redis 进程被杀掉 → 告警 + 自动重启
maxmemory 2gb
maxmemory-policy allkeys-lru
save 900 1 300 10 60 10000
groups:
- name: redis_alerts
rules:
- alert: RedisLowHitRate
expr: |
(redis_keyspace_hits_total /
(redis_keyspace_hits_total + redis_keyspace_misses_total)) < 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "Redis 缓存命中率低于 80%"
- alert: RedisMemoryHigh
expr: redis_memory_used_bytes / redis_max_memory_bytes > 0.85
for: 3m
labels:
severity: critical
annotations:
summary: "Redis 内存使用超过 85%"
总结
Redis 缓存失效的原因很多,但大多数问题都有成熟的解决方案:
- 惰性删除+过期时间设置错误 → 改用主动更新 + 随机 TTL
- 内存淘汰策略选错 → 根据业务场景选 LRU/LFU,按需配置
- 连接断开 → 客户端做好重连 + 连接池
- 穿透/击穿/雪崩 → 布隆过滤器 + 分布式锁 + 两级缓存
- 分布式锁失效 → Lua 脚本释放 + 自动续期机制
缓存是系统性能的核心,做好监控和降级比出了问题再救要重要得多。
如果你需要快速生成带盐值的哈希或者加密数据来测试你的缓存系统,可以试试我整理的 在线工具合集——包含 Hash 生成器、AES/RSA 加密、UUID 生成等实用工具,全部免费:
👉 CloverTools - 加密哈希工具
半夜爬起来修 Bug 很累,提前把缓存设计好,能省掉很多头发。
常见问题
Q: 如何使用 redis缓存失效翻车实录 相关工具?
A: 这类工具一般有明确的输入框和输出框,按提示输入内容,点击对应按钮即可得到结果。建议先用简单示例测试功能是否正常,再处理实际数据。
Q: redis缓存失效翻车实录 适合在什么场景使用?
A: 根据具体工具类型决定。格式转换工具适合处理第三方数据,编码工具适合加密传输,压缩工具适合文件上传前处理。多积累工具使用经验,遇到问题时能快速判断用哪个工具解决。
Q: 有没有更好的替代工具?
A: 不同工具有不同侧重,重点是理解原理。可以同时安装多个类似工具,实际使用中对比效果,选择最顺手的一个。随着使用经验增加,你也能判断工具的好坏。