雷灵模板

Redis 缓存穿透、击穿、雪崩实战处理:我后来是这样处理的

author
·
419
0
🤖AI摘要
本文探讨了Redis缓存穿透、击穿、雪崩的实战处理方法。作者强调,处理这些问题不能只看概念,要关注细节,如症状、配置、资源、数据分布等。在处理问题时,应先观察现象,确认问题所在,避免盲目操作。文章还提供了实用的排查命令和步骤,强调在生产环境中要注重细节,避免“差不多”心态,确保问题解决。

Redis 这三个问题,很多人第一次接触时容易混成一团:缓存穿透、缓存击穿、缓存雪崩。名字像,表现也像,线上一出事,排查的人很容易先往 Redis 上面猜,最后却发现问题出在业务判断、过期策略或者保护措施没做完整。

我后来处理这类问题的思路很简单:先分清是“查不到数据”还是“热点数据扛不住”,再看是不是“很多 key 一起过期了”或者“Redis 自己先不稳定”。这几个点分开看,处理办法其实不一样。

一、先把三个概念说清楚

缓存穿透,指的是请求的数据在缓存里没有,在数据库里也没有。比如有人一直拿一个不存在的 id 去查,缓存 miss,数据库也 miss,每次都穿过缓存打到数据库,量一大,数据库会被白白消耗。

缓存击穿,通常是某个热点 key 突然过期了,恰好这个 key 又特别热。过期那一瞬间,大量请求一起打到数据库,像在缓存上开了一个洞。

缓存雪崩,是更大范围的问题。大量 key 在同一时间失效,或者 Redis 整个服务挂了,结果原本应该被缓存挡住的流量,成片地落到数据库上。

这三种问题看上去都像“Redis 不工作了”,但真正的处理方式差别很大。穿透要防无效请求,击穿要防热点失守,雪崩要防批量失效和整站级联。

二、缓存穿透:最常见,也最容易被忽略

穿透一般不是 Redis 的错,而是系统没有拦住那些本来就不该继续往下走的请求。比如用户输入错误、爬虫扫描、恶意探测,甚至是前端参数没校验好,都会把不存在的 id 发给后端。

线上真出问题时,你会看到几个很典型的信号:缓存命中率明显下降,数据库的简单查询骤增,而且这些查询返回的结果几乎都是空。更麻烦的是,这种流量看起来“不重”,但因为全是无效请求,完全是在浪费资源。

我一般会先上这三层防线:

  • 参数校验,先把明显非法的请求挡在最前面。
  • 缓存空值,对短时间内重复查不到的数据做一个短 TTL 的空对象缓存。
  • 布隆过滤器,把明显不存在的数据直接拦掉。

1. 参数校验先做掉

这一步听起来很普通,但它的性价比其实最高。比如用户 id、商品 id、文章 id 这些字段,很多场景下本来就有范围限制。你在进入缓存之前做一次校验,能省掉不少无意义的查库。

// 伪代码
if (id <= 0) {
    return null;
}
if (id > MAX_ID) {
    return null;
}

别小看这个动作。很多系统真正的穿透,不是攻击很强,而是前面的入口太松。

2. 空值缓存要有,但 TTL 不能太长

如果某个 key 查不到,你可以把一个空值缓存起来,比如缓存 30 秒到 2 分钟。这样同一个不存在的请求短时间内不会反复打数据库。

但空值缓存不是越久越好。时间太长,会让新数据创建后迟迟看不到。这个问题我见过不少次:为了防穿透,把空对象缓存了半小时,结果用户刚新建的内容,页面还一直显示不存在。

所以我更倾向于短 TTL + 适当随机抖动。对不存在的数据,缓存时间短一点够用,别让它变成另一个一致性问题。

3. 布隆过滤器适合挡大批量无效请求

如果你的数据量比较大,或者外部请求很杂,布隆过滤器会比单纯的空值缓存更稳一些。它的思路是:先把“可能存在”的 id 放进过滤器里,查不到的直接拒绝。

它的缺点也要认:有一定误判率,也就是会把少量本来存在的数据误判成不存在。所以它更适合做前置拦截,不适合单独承担最终判断。

三、缓存击穿:热点 key 到点失守

击穿和穿透最大的区别,在于这里的数据本来是存在的。只是缓存到期了,热门请求又刚好集中到这个 key 上。于是缓存层空了,数据库突然被推上前台。

这类问题最典型的场景,就是首页推荐、秒杀库存、热门商品详情、活动配置页。只要某个 key 足够热,一过期就会有人一起去重建缓存。

我见过一种特别常见的写法:缓存过期后,第一个请求去查数据库并重建缓存,其他请求直接放过去。这个逻辑看上去没错,但在高并发下很容易炸,因为第一个请求还没来得及写回缓存,后面一批请求已经同时打到数据库了。

解决击穿,通常有两条路:互斥锁逻辑过期

1. 互斥锁:简单直接,适合大多数场景

思路就是:缓存 miss 之后,先抢锁,抢到的人去查数据库并回填缓存;没抢到的人先等一下,再重新读缓存。这样可以避免大家一起冲进数据库。

// 伪代码
String key = product:1001;
String value = redis.get(key);
if (value != null) return value;

boolean locked = tryLock(lock:product:1001);
if (!locked) {
    sleep(50);
    return redis.get(key);
}
try {
    value = db.queryProduct(1001);
    redis.set(key, value, 300, TimeUnit.SECONDS);
    return value;
} finally {
    unlock(lock:product:1001);
}

这个方案的优点是直观,缺点也很明显:高峰期会有一批请求排队,延迟会上升。不过比数据库直接被打穿要好得多。

2. 逻辑过期:更适合热点很高的读多写少数据

逻辑过期的做法更像“先继续用旧数据,再后台更新”。缓存里不直接存业务对象,而是存一个对象和一个过期时间。请求进来时,如果发现逻辑过期,就先返回旧数据,同时异步刷新缓存。

这个办法的好处是,用户几乎感受不到抖动。坏处是实现复杂一点,而且你得接受短时间内返回旧值。

// 伪代码
class CacheData {
    Object data;
    long expireAt;
}

CacheData cache = redis.get(key);
if (cache != null && cache.expireAt > System.currentTimeMillis()) {
    return cache.data;
}

if (cache != null && tryLock(lock: + key)) {
    asyncReload(key);
    return cache.data;
}

如果你问我怎么选:普通业务优先互斥锁,特别热的读多写少场景再考虑逻辑过期。别一上来就把方案做得很重,没必要。

四、缓存雪崩:真正危险的是“同时失效”

雪崩比击穿更麻烦,因为它不是一个点,而是一片。常见原因有两个:一是很多 key 设置了相同或接近的过期时间,二是 Redis 自己出问题,导致缓存层整体失效。

如果大量 key 同一秒过期,数据库就像突然少了一层缓冲;如果 Redis 整个挂掉,情况更直接,所有请求都会往下游走。这个时候,数据库、接口线程池、连接池,任何一个地方都可能先顶不住。

1. 给 TTL 加随机抖动

这是最实用的一招。别让 key 在同一个整点失效,TTL 可以加一点随机值,比如基础 10 分钟,再加 0 到 5 分钟随机时间。

// 伪代码
int baseTtl = 600;
int randomExtra = new Random().nextInt(300);
redis.set(key, value, baseTtl + randomExtra, TimeUnit.SECONDS);

这一点很朴素,但真的能救命。很多雪崩不是系统设计很差,而是大家习惯了把 TTL 写得整整齐齐。

2. 热点数据分层,别把鸡蛋放一个篮子里

如果业务很重,建议做多级缓存:本地缓存 + Redis + 数据库。这样就算 Redis 抖一下,本地缓存还能顶住一部分请求。热门配置、基础字典、活动信息这类内容,尤其适合这么做。

不过多级缓存不是白送的,数据一致性会复杂一些。你得明确哪些数据可以短时间不一致,哪些数据必须实时更新。别什么都塞进去,最后把自己绕晕。

3. Redis 真挂了,也要有降级策略

很多系统平时一直盯着缓存命中率,却忘了 Redis 真的挂了以后怎么办。比较稳的做法是:在网关、服务层或者中间件层提前准备降级逻辑。比如直接返回默认数据、关闭非核心功能、把写请求打到消息队列里缓慢恢复。

如果你没有降级,雪崩就不只是数据库压力大,而是整条链路一起倒。

五、我常用的一套生产处理顺序

这几年碰过几次缓存事故后,我自己慢慢形成了一套固定顺序:

  1. 先确认是穿透、击穿还是雪崩,不要一上来就重构。
  2. 先补入口校验,再看空值缓存和布隆过滤器。
  3. 热点 key 用互斥锁,特别热的读多写少数据再考虑逻辑过期。
  4. TTL 不要整齐划一,必须加随机抖动。
  5. 核心数据准备降级和兜底,别把 Redis 当成唯一防线。

很多线上问题不是技术难,而是顺序乱。你如果先想着“要不要上布隆过滤器”,却连最基本的参数校验都没做好,系统会绕很多弯。

六、几个容易踩的坑

第一个坑:空值缓存时间太长。为了省一次查库,把不存在的数据缓存太久,结果业务数据新增后迟迟不刷新。

第二个坑:锁写得不严。抢锁失败后直接重试,没有退避,最后变成大量线程空转。

第三个坑:把所有 key 的 TTL 都写成一样。测试环境看不出来,真实流量一波过去,问题集中爆发。

第四个坑:只做缓存,不做降级。Redis 正常时一切都好看,Redis 一旦波动,整套系统没有第二方案。

七、给一个可以直接落地的判断标准

如果你现在就要开始改,我建议先按下面这个标准做:

  • 对不存在的数据,先做参数校验,再考虑空值缓存。
  • 对高频热点数据,先加互斥锁,不要让很多请求一起查库。
  • 对大批量缓存 key,TTL 一定加随机值。
  • 对核心链路,准备降级方案。

这样做不花哨,但基本能把 Redis 相关的三类常见事故压住。等系统真的稳定下来,再去考虑更复杂的多级缓存、异步重建、热点探测这些东西。

说到底,缓存这件事不是炫技。最有价值的,通常是把每一层防线都补到位。只要入口、缓存、数据库之间的边界清楚了,很多看似复杂的问题,其实会安静很多。

评论 (0)