为什么你的Redis总崩?5个血泪教训

  • 约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倍性能,用错了分分钟把系统搞崩。我们在每个场景都踩过坑,现在的经验是:宁可少用缓存,也别用错。

你遇到过哪些缓存问题?评论区聊聊。

相关文章

代码重构不再头疼:AI帮你自动化

代码重构总是耗时耗力、不敢下手?本文分享如何用AI工具分钟级完成代码重构,列出具体工具、步骤和取舍建议,帮助开发者低成本提升代码质量。

查看更多

后海滑冰知识准备

从小在长江边上长大,有20年以上的游泳史,却一直因为不会正确呼吸而仰着头游泳,游得既累又慢。直到前段时间看了一段游泳教学视频,详细了解了换气的方法和要领。再经过两小时的水下实践,终于可以在游泳时正确的呼吸了。

查看更多

比特币文摘上线

第一次听说比特币是在年初,当时了解到的价格是$25左右,没有看任何技术资料,就想当然的以为是电子玩具或者庞氏骗局。

查看更多