Redis缓存雪崩怎么解决?随机过期时间设置不管用?

Mc.振莉 阅读 69

最近在优化项目缓存时遇到个难题:我们用了Redis存热点数据,但发现大量key会在同一时间集中过期。昨天测试时,设置了统一30分钟过期时间的用户信息缓存突然全失效,导致数据库瞬间被打爆。

我试过给过期时间加随机值,像这样:EXPIRE user:123 300 + Math.random()*60,但发现有些key还是在相近时间过期。看监控发现,即使分散了时间,高峰期还是有20%的缓存同时失效。

还有同事建议用双Redis集群做互备,但具体怎么实现呢?比如主库缓存过期时,从库如何接管?另外,缓存更新时要不要加锁?之前用Lua脚本锁的时候,偶尔会出现死锁错误,像这样:


async function getCacheWithLock(key) {
  await redis.evalsha(lockScript, 0, key); // 这里偶尔报错
  try {
    const data = await fetchDataFromDB();
    await redis.set(key, data, 'EX', 1800);
  } finally {
    redis.del(key + ':lock');
  }
}

感觉现在方案都不够稳妥,有没有更可靠的雪崩防护方案?

我来解答 赞 10 收藏
二维码
手机扫码查看
2 条解答
南宫娜娜
缓存雪崩的核心问题是大量 key 同时过期,你加的随机数没用,是因为 EXPIRE 是个精确命令,不是 TTL 随机值。你那句 EXPIRE user:123 300 + Math.random()*60 只是给客户端拼了个数发过去,Redis 收到的是一个固定时间戳,所以还是可能集中在某个窗口过期。

---

### ✅ 正确解决方案如下:

#### 1. **使用 Redis 的 EXPIRE 指令 + 随机 TTL**
不是拼时间戳,而是拼 TTL(单位秒),这样每个 key 的过期时间才是真正的随机。

const ttl = 1800; // 30分钟
const randomOffset = Math.floor(Math.random() * 600); // 0~10分钟随机偏移
await redis.expire(key, ttl + randomOffset);


或者更简洁点,用 SETEX 一起设置:

await redis.setex(key, ttl + Math.floor(Math.random() * 600), data);


---

#### 2. **热点数据永不过期(主动刷新)**
对特别热点的数据,可以设成永不过期,后台异步刷新。比如:

await redis.set(key, data, 'EX', 0); // 永不过期
// 然后用定时任务去更新


再配合一个定时器,每隔 10 分钟去检查是否需要刷新,这样就不会出现集中失效。

---

#### 3. **缓存层加降级和熔断**
雪崩来了先拦住,别直接打 DB。用 Redis + 本地缓存兜底,比如:

const localCache = new Map();

async function getCachedData(key) {
const local = localCache.get(key);
if (local) return local;

const remote = await redis.get(key);
if (remote) {
localCache.set(key, remote, 60_000); // 本地缓存1分钟
return remote;
}

// 如果 Redis 没有,再走 DB,记得加锁
return fetchDataFromDBAndCache(key);
}


---

#### 4. **缓存更新加锁防击穿**
你那段 Lua 脚本加锁方式是错的,Redis 的 Lua 脚本是原子的,但 del 不在脚本里,容易出问题。推荐用 Redis 官方支持的锁方案,比如:

const lockKey = key + ':lock';
const lockTimeout = 1000; // 1s

const acquired = await redis.set(lockKey, 'locked', 'NX', 'PX', lockTimeout);
if (acquired) {
try {
const data = await fetchDataFromDB();
await redis.setex(key, 1800, data);
} finally {
await redis.del(lockKey);
}
} else {
// 没抢到锁,走本地缓存或等待
}


---

#### 5. **双集群互备?真没必要,除非你业务真的特别大**
小项目别折腾双集群,维护成本高,而且你得处理一致性、同步、路由逻辑。除非你 QPS 上了几十万,不然别整这种花活。

---

### ✅ 总结

你目前的方案问题在于:
- 过期时间没真正随机
- Lua 加锁逻辑不完整
- 没有兜底机制

拿去改改这几个点,雪崩问题基本就能解决。

> 我以前也踩过这些坑,改完上线后监控曲线稳如老狗。
点赞 9
2026-02-04 13:11
程序猿佩佩
Redis缓存雪崩确实是个让人头疼的问题,尤其是高峰期突然打爆数据库的时候,分分钟让你心惊肉跳。不过别担心,我来给你支几招。

首先,你说的随机过期时间其实是个不错的思路,但可能你的随机范围还不够大。我的做法是把随机时间设置得更离散一些,比如:

const baseTTL = 30 * 60; // 30分钟基础过期时间
const randomTTL = Math.floor(Math.random() * (10 * 60)); // 随机加0到10分钟
await redis.expire(key, baseTTL + randomTTL);


这样可以让key的过期时间分布得更均匀,减少同时失效的概率。

关于双Redis集群互备的方案,其实实现起来不复杂。你可以用一个主Redis存储正常缓存,另一个备用Redis作为“影子”缓存。当主缓存过期时,先查备用缓存;如果备用缓存也没有,则从数据库加载数据,并同时更新主备两个缓存。代码大概是这样的:

async function getWithDoubleCache(key) {
let data = await primaryRedis.get(key);
if (!data) {
data = await secondaryRedis.get(key);
if (!data) {
data = await fetchDataFromDB();
await Promise.all([
primaryRedis.set(key, data, 'EX', ttl),
secondaryRedis.set(key, data, 'EX', ttl)
]);
} else {
// 更新主缓存,防止备用缓存过期后出现空窗
await primaryRedis.set(key, data, 'EX', ttl);
}
}
return data;
}


至于你提到的Lua脚本锁问题,死锁的情况可能是锁超时设置不合理导致的。建议你在Lua脚本里加上超时时间,避免锁一直占着不释放。改写后的脚本可以这样:

local lockKey = KEYS[1]
local identifier = ARGV[1]
local expiration = tonumber(ARGV[2])

-- 尝试加锁,设置超时时间
if redis.call("SETNX", lockKey, identifier) == 1 then
redis.call("PEXPIRE", lockKey, expiration)
return 1
else
-- 检查现有锁是否已经过期
local remainingTime = redis.call("PTTL", lockKey)
if remainingTime < 0 then
-- 锁已过期,重新设置
redis.call("SET", lockKey, identifier)
redis.call("PEXPIRE", lockKey, expiration)
return 1
end
end
return 0


最后再啰嗦一句,除了技术手段,平时也要多关注监控和压测结果,提前发现问题苗头。毕竟预防总是比事后补救更靠谱,你说是不是?
点赞 3
2026-01-31 18:00