- 约1548字
- 技术
- 2026年5月3日
很多人觉得Redis稳,但其实它崩起来比数据库还狠。
上个月我们线上一次Redis缓存雪崩,直接导致数据库被打挂,全站响应时间从200毫秒飙升到30秒。那天我正在吃饭,监控报警响个不停,后台显示大量请求穿透到MySQL,业务直接不可用了。
后来复盘发现,问题不是Redis本身不稳,而是我们没有正确使用它。本文总结5个我踩过的Redis缓存坑,给出可直接套用的解决方案。
场景1:缓存雪崩 —— 同一时间全部过期
表现: 大量缓存key在同一秒过期,瞬间大量请求穿透到数据库。
我们的教训: 当时给所有缓存设置了相同的过期时间(2小时),凌晨2点一到,缓存集体失效,所有请求直接打DB。
解决方案: 给缓存过期时间加随机偏移量。
// 错误写法
redis.setex('user:123', 7200, data);
// 正确写法:过期时间 = 基础时间 + 随机偏移
const ttl = 7200 + Math.floor(Math.random() * 1800);
redis.setex('user:123', ttl, data);
或者在Redis 2.1.4以上版本,设置 persist 参数为 -1,禁用特定key的过期策略,这是我们目前的做法。
场景2:缓存击穿 —— 热点 key 过期瞬间
表现: 一个热点key过期瞬间,大量并发请求同时查数据库。
我们的教训: 一个热门商品详情页的缓存过期重建时,并发请求直接打到数据库,最高纪录单秒3000次查询,数据库CPU直接打满。
解决方案: 使用永不过期 + 被动更新的策略。
async function getProduct(id) {
const cacheKey = `product:${id}`;
let data = await redis.get(cacheKey);
if (data) {
return JSON.parse(data);
}
// 使用setNx实现互斥锁,只有1个请求去查库
const lock = await redis.setNx(cacheKey + ':lock', '1', 'EX 10');
if (!lock) {
// 其他请求等待100ms后重试
await sleep(100);
return getProduct(id);
}
// 只有拿到锁的去查库
const dbData = await db.query('SELECT * FROM products WHERE id = ?', [id]);
await redis.setex(cacheKey, 86400, JSON.stringify(dbData));
await redis.del(cacheKey + ':lock');
return dbData;
}
如果使用Redisson,配置更简单,直接用 RLock 就能实现分布式锁。
场景3:缓存穿透 —— 查了一个不存在的值
表现: 查询一个数据库根本不存在的key,每次都穿透到数据库。
我们的教训: 当时的风控系统会查询大量不存在的用户ID,这些请求每次都打到数据库,最高一天几百万次空查询,数据库慢得像蜗牛。
解决方案: 布隆过滤器 + 空值缓存。
// 第一层:布隆过滤器检查是否存在
if (!bloomFilter.mightContain(`user:${userId}`)) {
return null; // 直接返回,不查数据库
}
// 第二层:缓存穿透后,返回空值也缓存
const data = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
if (data) {
await redis.setex(`user:${userId}`, 3600, JSON.stringify(data));
} else {
// 不存在的数据,缓存一个特殊标记,过期时间设短点
await redis.setex(`user:${userId}`, 60, 'NULL');
}
return data;
布隆过滤器推荐使用Guava的 BloomFilter 或 Redis的Bloom模块(Redis 4.0以上支持)。
场景4:内存溢出 —— key 越来越多
表现: Redis内存持续增长,直到OOM。
我们的教训: 当时我们做实时推荐,缓存了大量用户行为数据,没有设置TTL,内存从2G涨到8G只用了一周,最后直接OOM重启。
解决方案: 强制设置TTL + 定期清理 + 内存监控。
// 命令行设置key过期时间
redis.expire(`user:behavior:${userId}`, 86400 * 7);
// 或者在业务层定期清理
async function cleanup() {
const keys = await redis.keys('user:behavior:*');
for (const key of keys) {
const ttl = await redis.ttl(key);
if (ttl < 0) {
await redis.del(key);
}
}
}
我们现在的做法是全量key默认设置30天过期,超时自动清理,配合告警监控内存使用量。
场景5:热点key —— 集中访问导致单点瓶颈
表现: 一个key被集中访问,单机Redis成为瓶颈。
我们的教训: 双十一期间,一个热销商品的缓存key访问量是其他key的100倍,单机Redis CPU直接跑满。
解决方案: 客户端hash到多副本。
// 将热点key分散到多个Redis实例
const replicas = [redis1, redis2, redis3];
function getHotData(key) {
// 用key的hash值分散到不同实例
const index = hash(key) % replicas.length;
return replicas[index].get(key);
}
function setHotData(key, value, ttl) {
// 写入所有副本
for (const redis of replicas) {
redis.setex(key, ttl, value);
}
}
更简单的方案是用Redis Cluster模式,自动将key散列到16384个槽位。
总结
这5个场景覆盖了缓存最常见的问题:
| 场景 | 根因 | 核心方案 |
|---|---|---|
| 雪崩 | 同时过期 | 随机偏移量 |
| 击穿 | 热点key | 互斥锁 |
| 穿透 | 空查询 | 布隆过滤器 |
| 溢出 | 无TTL | 强制过期 |
| 热点 | 集中访问 | 多副本 |
缓存不是银弹,用好了能提升10倍性能,用错了分分钟把系统搞崩。我们在每个场景都踩过坑,现在的经验是:宁可少用缓存,也别用错。
你遇到过哪些缓存问题?评论区聊聊。