Redis 缓存为什么会出问题?穿透、击穿、雪崩与缓存一致性

上一篇我们介绍了 Redis 的基本工作原理,包括 String、Hash、List、Set、ZSet,以及 TTL、内存淘汰和持久化机制。

在微服务系统中,Redis 最常见的用途之一仍然是缓存。

比如电商系统中的商品详情、用户信息、商品分类等数据,往往会被大量重复读取。我们可以在 MySQL 前面增加 Redis,让部分请求直接从内存获取数据。

但问题也随之出现。

如果 Redis 里没有数据怎么办?如果一个热门商品的缓存突然过期,几万个请求会不会同时访问 MySQL?如果数据库里的商品价格修改了,Redis 为什么有时还会返回旧价格?

这些问题归根结底可以分为两类:

一类是缓存无法有效拦截请求,导致数据库承受过大的访问压力。

另一类是缓存与数据库中的数据不一致,导致用户读取到错误或过时的信息。

前者对应缓存穿透、击穿和雪崩,后者则涉及缓存更新策略和并发一致性。

理解这些问题,首先要从最常见的缓存使用方式开始。

一、先理解 Cache Aside:Redis 和 MySQL 是怎么配合的?

假设 Product Service 负责提供商品详情查询。

最开始,所有请求都直接访问 MySQL:

text 复制代码
Client
   |
   v
Product Service
   |
   v
MySQL

用户查询商品:

sql 复制代码
SELECT id, name, price
FROM products
WHERE id = 888;

如果商品 888 每秒被访问几千次,那么数据库就需要不断处理重复查询。

引入 Redis 后,可以把商品信息缓存起来:

text 复制代码
Client
   |
   v
Product Service
   |
   v
Redis
   |
   | 未命中
   v
MySQL

但 Redis 不会自动替我们完成所有缓存逻辑,应用需要明确规定什么时候读取缓存、什么时候访问数据库,以及什么时候更新缓存。

我们之前学习过的 Cache Aside(旁路缓存),就是一种常见的缓存管理模式。

1. 读取数据:先查缓存,再查数据库

例如用户请求:

http 复制代码
GET /products/888

Product Service 首先执行:

redis 复制代码
GET product:888

如果 Redis 返回了有效商品数据,就直接返回给用户。

如果 Redis 中没有这个 Key,则访问 MySQL:

sql 复制代码
SELECT id, name, price
FROM products
WHERE id = 888;

得到查询结果后,再写入 Redis:

redis 复制代码
SET product:888 '{"id":888,"name":"Keyboard","price":199}' EX 300

最后将结果返回给用户。

完整流程:

text 复制代码
                 查询商品 888
                       |
                       v
                  Redis GET
                       |
                +------+------+
                |             |
              命中           未命中
                |             |
                v             v
             返回数据        MySQL
                              |
                              v
                           查询商品
                              |
                              v
                         写入 Redis
                              |
                              v
                           返回数据

这里有个很重要的细节:

缓存不是直接从数据库里"长出来"的,而是由应用根据查询结果主动填充。

因此,如果数据库已经更新,但 Redis 没有同步处理,就可能继续保存旧值。

2. 更新数据:先更新 MySQL,再删除缓存

假设商品价格从 199 元调整为 179 元。

我们一般不建议直接把 Redis 当作唯一的商品价格来源。

对于这个例子,MySQL 是商品价格的权威存储。

更新操作可以采用:

text 复制代码
更新 MySQL
     |
     v
事务提交成功
     |
     v
删除 Redis 中对应缓存

例如:

sql 复制代码
UPDATE products
SET price = 179
WHERE id = 888;

提交成功后:

redis 复制代码
DEL product:888

注意这里是删除缓存,而不是立即把新价格写入 Redis。

因为缓存被删除后,下一个查询自然会重新访问 MySQL,并把最新结果填充到 Redis 中。

这就是常见的:

Cache Aside:读时按需填充缓存,写时更新数据库并使缓存失效。

它的优点是实现相对简单,缓存不需要保存所有数据库记录,只需要保留真正发生读取的数据。

但它也存在两个问题。

第一,缓存未命中时,请求还是会进入 MySQL。如果大量请求同时未命中,就可能产生数据库压力。

第二,MySQL 和 Redis 是两个不同的系统,更新数据库与删除缓存并不是天然的原子操作。

下面分别讨论这两个问题。

二、缓存穿透:查询的数据根本不存在

假设 Product Service 提供商品查询接口:

http 复制代码
GET /products/{id}

正常情况下,商品 888 存在于 MySQL 中。

第一次查询没有命中 Redis:

text 复制代码
Redis:没有 product:888
        |
        v
MySQL:商品 888 存在
        |
        v
写入 Redis

之后再查询商品 888,就可以直接命中缓存。

现在换一个情况。

用户查询:

http 复制代码
GET /products/999999999

但数据库中根本不存在这个商品。

请求流程就变成:

text 复制代码
Redis:没有 product:999999999
        |
        v
MySQL:也没有这个商品
        |
        v
返回不存在

如果应用只缓存存在的商品,那么下次请求同一个不存在的 ID 时:

text 复制代码
Redis:仍然没有
        |
        v
MySQL:仍然不存在

每次请求都会穿过 Redis,进入数据库。

这就是缓存穿透(Cache Penetration)。

它的核心问题不是某个缓存过期,而是:

查询目标根本不存在,导致这类请求始终无法建立正常的有效数据缓存。

缓存穿透为什么危险?

正常用户偶尔查询一个不存在的商品,其实影响不大。

但如果大量请求持续查询不存在的商品 ID:

text 复制代码
product:99990001
product:99990002
product:99990003
...

Redis 可能始终无法命中。

最终:

text 复制代码
大量无效请求
      |
      v
Redis 持续 Miss
      |
      v
MySQL 持续处理不存在的数据查询

虽然每条 SQL 可能执行得很快,但大量请求仍然会消耗数据库连接、CPU 和 I/O 等资源。

解决方法一:缓存空值

最简单的方式,是把"数据不存在"这个结果也短暂缓存起来。

例如:

redis 复制代码
SET product:999999999 "__NULL__" EX 60

这里的 __NULL__ 是我们约定的空值标识。

下一次查询时:

redis 复制代码
GET product:999999999

如果得到 __NULL__,就直接返回商品不存在,而不再查询数据库。

这样,同一个不存在的商品 ID 在缓存有效期内就不会反复访问 MySQL。

但空值缓存不能随意设置很长时间。

假设商品 999 原来不存在,我们缓存了:

text 复制代码
product:999 = __NULL__

随后管理员创建了这个商品。

如果空值缓存还没有过期,用户仍然可能看到"商品不存在"。

因此,空值通常适合设置较短的 TTL,并在新增对应数据时主动清理相关缓存。

解决方法二:Bloom Filter

如果系统存在大量不同的无效 ID,仅靠缓存空值可能会产生很多无意义的缓存项。

这时可以考虑 Bloom Filter(布隆过滤器)。

它是一种用于快速判断元素是否可能存在的数据结构。

例如先把数据库中已有的商品 ID 纳入过滤器:

text 复制代码
商品 1001
商品 1002
商品 1003
商品 1004

用户查询商品 999999 时,先判断:

text 复制代码
Bloom Filter
      |
      v
这个 ID 是否可能存在?

如果过滤器确定它不存在,就直接拒绝进一步查询。

如果过滤器判断它可能存在,再继续查询 Redis 或 MySQL。

这里必须理解 Bloom Filter 的一个特点:

它可能把不存在的元素判断为"可能存在",但在数据已正确纳入过滤器的前提下,不会把已纳入的元素错误判断为不存在。

因此,它适合做前置过滤,但不能直接代替数据库作为商品是否存在的最终判断依据。

另外,当数据库新增商品时,过滤器也需要及时更新,否则新数据可能被错误拦截。

应该怎么选?

如果只是少量重复查询不存在的 ID,缓存空值通常已经足够。

如果存在大量不同的无效 ID,而且无效请求占比很高,可以考虑增加布隆过滤器。

此外,对于恶意枚举 ID 或异常请求,还需要在入口层进行合理的限流和请求校验。

缓存穿透不一定要通过复杂的数据结构解决,首先应该看无效查询到底是怎么产生的。

三、缓存击穿:一个热门 Key 突然失效

缓存穿透针对的是不存在的数据。

缓存击穿则不同。

它通常发生在一个非常热门、原本可以命中的缓存 Key 突然失效时。

假设商品 888 是一款热门商品:

text 复制代码
product:888

每秒有几千个请求查询它。

正常情况下:

text 复制代码
大量请求
    |
    v
Redis 命中 product:888
    |
    v
直接返回

MySQL 不需要承担这些重复查询。

但假设这个 Key 设置了 300 秒 TTL。

某一时刻:

text 复制代码
product:888 过期

恰好大量用户继续查询它。

结果:

text 复制代码
Request A ──┐
Request B ──┤
Request C ──┤
Request D ──┼──> Redis Miss
Request E ──┤          |
    ...    ──┘          v
                       MySQL

本来应该由 Redis 承担的高频访问,突然全部转移到 MySQL。

这就是缓存击穿(Cache Breakdown)。

这里和缓存穿透的关键区别是:

缓存穿透查询的是根本不存在的数据。

缓存击穿查询的是确实存在、访问频率很高,但缓存暂时缺失的数据。

为什么不让所有请求一起回源?

假设商品详情 SQL 一次需要 20ms。

如果只有一个请求回源,数据库压力很小。

但如果同一时刻有几千个请求全部去查询同一个商品,就会造成不必要的并发负载。

实际上,我们只需要有一个请求从 MySQL 读取商品信息,然后写回 Redis。

其他请求等到缓存恢复后,再从 Redis 获取数据即可。

因此,一个常见方法是:

互斥重建缓存。

例如:

text 复制代码
大量请求发现缓存不存在
          |
          v
     竞争缓存重建资格
          |
     +----+----+
     |         |
   成功       未获得
     |         |
     v         v
查询 MySQL    短暂等待
     |         |
     v         |
写入 Redis     |
     |         |
     +----+----+
          |
          v
      读取缓存

这样可以减少同一个热门 Key 被大量重复加载的问题。

在工程实现中,可以使用进程内的 Singleflight 机制,也可以使用 Redis 的带过期时间的互斥标记。

但如果服务部署了多个实例,仅靠某个实例内部的互斥机制,无法协调其他实例。

使用 Redis 互斥标记时,也需要设置合理的过期时间和持有者标识,避免某个重建请求崩溃后长期阻塞其他请求。

这里我们只讨论缓存重建的并发控制,关于 Redis 分布式锁的完整正确性问题,会在后面的分布式锁专题深入介绍。

另一种方法:允许短暂使用旧缓存

如果业务允许短时间读取稍旧的数据,也可以采用一种思路:

缓存到达刷新时间后,不立即让所有请求回源,而是继续提供可接受的旧数据,同时由少量请求负责刷新。

例如:

text 复制代码
缓存数据存在
      |
      v
判断业务刷新时间
      |
   已经过期
      |
      +------ 返回允许使用的旧数据
      |
      +------ 后台刷新新数据

这种思路通常称为 Stale-While-Revalidate。

它更适合:

text 复制代码
商品介绍
非实时排行榜
内容推荐

之类可以容忍短暂过时的场景。

但如果是支付状态、库存权威数量或其他要求严格正确的数据,就不能随意把旧数据返回给用户。

此外,后台刷新还需要处理任务失败、重复刷新等问题。

所以缓存击穿并没有唯一的解决方案。

真正需要根据业务决定的是:

缓存失效时,应该等待最新数据,还是允许先返回稍旧的数据。

四、缓存雪崩:大量 Key 同时失效

前面缓存击穿讨论的是一个热门 Key。

现在假设 Redis 中保存了大量商品缓存:

text 复制代码
product:1001
product:1002
product:1003
product:1004
...

这些 Key 恰好在同一时间被批量写入,并且全部设置:

text 复制代码
TTL = 300 秒

五分钟以后:

text 复制代码
product:1001 过期
product:1002 过期
product:1003 过期
product:1004 过期
...

大量缓存集中失效。

于是本来分散在 Redis 中的读取请求,突然一起访问 MySQL。

text 复制代码
                 大量不同请求
                       |
                       v
                  Redis 大量 Miss
                       |
                       v
                  大量请求回源
                       |
                       v
                     MySQL
                       |
                       v
                   压力骤增

这就是**缓存雪崩(Cache Avalanche)**的一种典型情况。

击穿和雪崩有什么区别?

两者都可能让大量请求访问数据库,但问题来源不同。

缓存击穿通常集中在一个或少量热点 Key 上。

缓存雪崩则是大量 Key 在短时间内一起失效,或者 Redis 服务整体不可用,导致大范围请求无法通过缓存得到结果。

所以不能简单认为它们只是请求数量不同。

解决方法一:给 TTL 增加随机值

假设我们原来给所有缓存设置:

text 复制代码
TTL = 300 秒

可以改成:

python 复制代码
ttl = 300 + random.randint(0, 120)

这样不同缓存的 TTL 就会分散在:

text 复制代码
300~420 秒

之间。

例如:

text 复制代码
product:1001 → 312 秒

product:1002 → 368 秒

product:1003 → 401 秒

product:1004 → 329 秒

它们就不容易在同一时刻全部过期。

这里的核心不是"必须增加 120 秒",而是避免大量缓存使用完全相同的过期时间并集中失效。

TTL 的基础时长和随机范围都应该根据业务数据的更新频率与一致性要求确定。

不过,TTL 随机化主要解决集中到期问题。

如果 Redis 实例本身不可用,它就无法解决问题。

解决方法二:提高 Redis 可用性

如果单个 Redis 实例发生故障:

text 复制代码
Redis ×
   |
   v
所有依赖 Redis 的服务都无法正常访问缓存

那么即使缓存 TTL 都不同,也无法继续读取这些数据。

因此,对于重要业务,可以根据需要使用 Redis 副本、故障切换等机制,提高缓存服务的可用性。

但高可用并不等于绝对不会发生中断,也不保证故障切换时每一条数据都不会丢失。

解决方法三:保护后端数据库

这是缓存雪崩处理中最容易被忽略的一点。

假设 Redis 已经不可用了。

如果应用采用简单策略:

text 复制代码
Redis 查询失败
      |
      v
所有请求直接访问 MySQL

就可能把 Redis 的故障进一步传播到数据库。

原本只是缓存层故障,最后导致 MySQL 也被打垮。

因此,缓存大规模失效时,还需要根据数据库的实际处理能力控制回源并发。

例如:

text 复制代码
Redis 不可用
      |
      v
限制数据库回源并发
      |
  +---+---+
  |       |
允许部分  拒绝或降级
  |       |
  v       v
MySQL    返回繁忙

对于允许降级的业务,也可以暂时返回默认内容、减少非核心字段,或者使用仍然可访问的旧数据。

但不能为了维持接口成功率而返回错误的支付金额、订单状态或库存结果。

这里就和前面学习的 Gateway 限流、熔断和降级联系起来了。

缓存雪崩真正需要防止的,不只是缓存 Miss,而是缓存故障继续拖垮后端数据库。

五、三个问题放在一起,应该怎么区分?

学到这里,最容易混淆的仍然是这三个名词。

可以直接看下面这张表:

问题 数据本身 缓存情况 主要风险 常见处理
缓存穿透 数据不存在 一直无法命中有效数据 无效请求持续访问 DB 空值缓存、布隆过滤器
缓存击穿 数据存在,且访问量很高 热点 Key 突然失效 并发集中回源 互斥重建、允许旧值并刷新
缓存雪崩 大量不同数据 大批 Key 失效或缓存服务故障 大范围数据库压力骤增 TTL 随机化、高可用、限流降级

用商品查询再串一次:

穿透:

用户一直查询不存在的商品 999999,Redis 和 MySQL 都查不到。

击穿:

热门商品 888 的缓存过期,大量用户同时查询商品 888。

雪崩:

成千上万个商品的缓存集中失效,大量不同商品查询一起进入 MySQL。

它们的共同结果都是数据库压力可能上升,但根因不同。

因此,排查时不能只看到 Redis 命中率下降,就判断一定发生了缓存雪崩。

我们还需要看:

text 复制代码
Miss 集中在少量热点 Key?
    |
    v
重点排查缓存击穿

Miss 集中在大量不存在的 Key?
    |
    v
重点排查缓存穿透

大量不同 Key 同时失效?
    |
    v
重点排查缓存雪崩

这种根据请求分布和数据状态判断问题类型的方法,比单纯记忆概念更实用。


六、缓存一致性:为什么 MySQL 更新成功,Redis 还是旧数据?

前面的三个问题主要关注缓存失效后数据库压力如何变化。

接下来讨论另一个更重要的问题:

缓存的数据到底对不对?

假设 MySQL 中有商品:

text 复制代码
product_id = 888

price = 199

Redis 中也缓存了:

text 复制代码
product:888

price = 199

现在管理员把商品价格修改为:

text 复制代码
price = 179

MySQL 更新成功后,如果 Redis 仍然保存 199,那么用户查询时就可能得到旧价格。

这就是最常见的缓存与数据库不一致问题。

1. 为什么不直接同时更新 MySQL 和 Redis?

一个很自然的想法是:

text 复制代码
更新 MySQL
    |
    v
更新 Redis

例如:

sql 复制代码
UPDATE products
SET price = 179
WHERE id = 888;

然后:

redis 复制代码
SET product:888 '{"id":888,"price":179}' EX 300

看起来很合理。

但两个系统之间没有天然的原子事务。

可能发生:

text 复制代码
MySQL 更新成功
       |
       v
Redis 更新失败

结果 MySQL 是新价格,Redis 仍然是旧价格。

并发修改时,还可能发生更新顺序错乱。

例如两个请求先后将商品价格修改为 179 和 169,但对应的 Redis 写入以相反的顺序完成,最后缓存反而保留了较旧的值。

因此,我们之前学习的 Cache Aside 一般采用:

先更新数据库,再删除缓存。

这样,应用不需要在写操作中直接维护一个可能发生并发覆盖的完整缓存值。

2. 为什么不能先删 Redis,再更新 MySQL?

假设当前:

text 复制代码
MySQL = 199

Redis = 199

现在有两个并发请求:

  • 请求 A:修改商品价格。
  • 请求 B:查询商品价格。

如果请求 A 先删除 Redis,再更新 MySQL,就可能出现下面的执行顺序:

text 复制代码
请求 A:删除 Redis 缓存
          |
          v
请求 B:查询 Redis,发现 Miss
          |
          v
请求 B:从 MySQL 读取旧价格 199
          |
          v
请求 B:把 199 写回 Redis
          |
          v
请求 A:更新 MySQL,价格变成 179

最后:

text 复制代码
MySQL = 179

Redis = 199

这时候缓存又变成旧数据了。

而且后续请求还会继续命中旧缓存,直到它被删除或过期。

因此,先删除缓存、再更新数据库,容易在两个操作之间被并发读请求重新写入旧数据。

3. 先更新 MySQL,再删除 Redis,就完全安全了吗?

也不是。

这种方式通常能减少一类并发问题,但不能保证绝对强一致。

假设当前:

text 复制代码
MySQL = 199

Redis:缓存已经过期

现在请求 A 查询商品价格:

text 复制代码
请求 A:
Redis Miss
    |
    v
查询 MySQL,读到 199

在请求 A 把 199 写回 Redis 之前,请求 B 开始修改价格:

text 复制代码
请求 B:
更新 MySQL 为 179
    |
    v
删除 Redis 缓存

随后,请求 A 才完成之前的缓存回填:

text 复制代码
请求 A:
把先前读到的 199 写入 Redis

最后:

text 复制代码
MySQL = 179

Redis = 199

你会发现,即使遵循:

text 复制代码
先更新数据库
再删除缓存

仍然可能因为读写操作交错,使旧数据重新进入缓存。

这也是 Cache Aside 特别值得理解的地方:

它是一种常见且实用的缓存管理策略,但并不是严格的跨系统强一致性协议。

它主要通过删除缓存,让后续读取重新获取权威数据。

在极端并发时序、缓存删除失败或数据库与缓存之间发生通信故障时,仍然需要额外措施。


七、缓存一致性到底应该怎么处理?

这里不要一上来就追求某种理论上最复杂的方案。

首先应该明确:

业务能容忍多长时间的数据不一致?

例如:

text 复制代码
商品介绍

即使短时间读到旧描述,通常也不会造成严重问题。

但:

text 复制代码
支付金额
库存扣减
订单状态

则不应该简单把 Redis 缓存当作最终判断依据。

因此,我们可以从几个层次考虑。

第一层:更新数据库后删除缓存

对于普通缓存场景,优先考虑:

text 复制代码
更新 MySQL
      |
      v
提交成功
      |
      v
删除 Redis 缓存

这样可以避免在写入时直接维护完整缓存数据,也降低并发缓存写入乱序的风险。

第二层:设置合理的 TTL

即使某次缓存删除没有成功,TTL 也可以限制旧缓存长期保留的时间。

例如:

redis 复制代码
SET product:888 "..." EX 300

如果后续没有其他更新操作,缓存最多会在相应 TTL 到期后失效。

但 TTL 只能提供过期边界,不能保证用户在这 300 秒内一定读取到最新数据。

而且如果旧数据被并发请求重新回填,它会从新的回填时刻开始计算 TTL。

所以 TTL 不是强一致性方案。

第三层:缓存删除失败时,不能直接忽略

例如:

text 复制代码
MySQL 更新成功
       |
       v
Redis 删除失败

这时如果应用直接忽略错误,旧缓存可能持续存在。

对于较重要的场景,可以记录失败,并通过有界重试、可靠的失效任务或其他补偿机制重新删除缓存。

如果需要确保数据库变更后一定留下可恢复的缓存失效任务,可以考虑把变更记录与业务修改放在同一个数据库事务中,再由后台任务可靠执行缓存删除。

这与后面分布式事务专题中的 Transactional Outbox 思想有联系,但这里不需要展开整个 Outbox 实现。

需要记住的是:

数据库事务只能保证本地数据库操作的原子性,不能直接把 Redis 删除也纳入同一个普通 MySQL 事务。

第四层:对关键业务直接查询权威数据

对于支付状态、库存决策等要求较高的业务,不一定应该依靠缓存提供正确性保证。

例如秒杀系统中,Redis 可以负责快速过滤无效请求,但最终的订单和库存约束仍然需要权威数据存储与事务机制保证。

商品展示可以使用缓存价格提高响应速度,但真正创建订单时,应按照业务规则从可信的价格来源确认成交金额,而不是无条件相信客户端传回来的价格或可能过期的展示缓存。

同样,用户完成重要写入后,如果需要立即看到自己的最新结果,可以根据业务设计读取主库或者采用其他能够满足读后写一致性要求的方式。

这比试图让所有 Redis 缓存都具备数据库级别的强一致性更加实际。


八、把缓存问题放回微服务系统中看

现在假设有三个服务:

text 复制代码
Product Service

Order Service

Payment Service

它们都使用 Redis,但缓存策略不应该完全一样。

Product Service:优先考虑读取性能

例如商品基础信息:

text 复制代码
Product Service
      |
      v
Redis
      |
    Miss
      |
      v
MySQL

这类数据可以考虑 Cache Aside、适当 TTL 和热点缓存重建控制。

如果是热门商品缓存失效,应当防止并发请求集中回源。

Order Service:注意订单状态的时效性

例如用户刚创建订单:

text 复制代码
Order Service
      |
      v
MySQL 写入订单
      |
      v
用户马上查询订单状态

这时候如果查询命中了旧缓存,用户可能看不到刚创建的订单或最新状态。

因此,订单状态缓存需要结合状态更新主动失效,并根据业务需要设计读后写一致性策略。

特别是支付确认、超时取消这些关键状态变更,不能仅依赖缓存是否更新来判断业务执行是否成功。

Payment Service:正确性优先

例如:

text 复制代码
支付状态 = UNKNOWN

这时候 Redis 中即使保存了一个历史结果,也不能直接据此决定是否再次扣款。

必须结合权威支付记录、幂等键以及支付渠道的实际状态进行确认。

这里最重要的原则是:

是否使用缓存,以及允许多长时间的不一致,应该由业务正确性要求决定,而不是所有接口统一设置一个 TTL。


九、实际排查时,应该先看哪些指标?

假设某天 MySQL 的查询 QPS 突然上升。

团队怀疑是 Redis 出了问题。

但这时不能立刻断定发生了缓存雪崩。

可以先看几个指标。

第一,缓存命中率是否明显下降?

如果命中率从正常水平突然大幅下降,说明大量请求开始无法直接从缓存中获取数据。

但还需要区分它们是少量热点 Key 失效,还是大量不同 Key 失效。

第二,Redis 自身是否正常?

例如:

text 复制代码
Redis 连接错误
Redis 请求超时
Redis 内存压力
Key 过期情况
Key 淘汰情况

如果整个 Redis 实例无法访问,问题可能是缓存服务故障,而不是简单的 TTL 设置不合理。

第三,MySQL QPS 和响应延迟是否同时上升?

如果缓存命中率下降,但数据库仍然可以稳定处理请求,系统可能只是出现了短暂回源增加。

如果数据库 QPS、连接池等待和 P99 同时上升,就需要重点防止故障扩散。

第四,Miss 请求集中在哪些 Key 上?

例如:

text 复制代码
大量 Miss 指向同一个 product:888

重点排查热点 Key 击穿。

如果:

text 复制代码
大量 Miss 指向不同的无效商品 ID

重点排查缓存穿透。

如果:

text 复制代码
大量不同有效商品 Key 集中失效

重点排查缓存雪崩。

所以,在工程实践中,缓存问题最好结合 Key 分布、命中率、回源 QPS 和数据库延迟一起分析。

只看一个整体缓存命中率,很难准确定位问题。

十、总结:Redis 缓存不只是提高性能,还要控制失效后的风险

Redis 缓存看起来很简单:

text 复制代码
先查 Redis
    |
    v
没有再查 MySQL

但真正复杂的地方,不在缓存命中时,而在它没有命中、突然失效或者与数据库状态不一致时。

这篇文章最重要的知识可以归纳为四个问题。

第一,查询的数据不存在怎么办?

这对应缓存穿透,可以通过空值缓存、布隆过滤器等方式减少无效请求回源。

第二,一个热门缓存突然失效怎么办?

这对应缓存击穿,需要避免大量并发请求同时重建同一份数据。

第三,大量缓存一起失效怎么办?

这对应缓存雪崩,需要通过 TTL 分散、高可用以及数据库回源保护,防止缓存故障继续影响后端。

第四,数据库修改后,怎样避免 Redis 保留旧数据?

这对应缓存一致性。

Cache Aside 通常采用先更新数据库、再删除缓存的策略,但仍然需要面对并发回填、删除失败和短暂不一致等情况。

最后需要记住:

Redis 缓存的价值不仅是让请求更快,还在于减少后端数据库的重复负载。而缓存设计是否可靠,取决于缓存失效时数据库能否承受,以及缓存和权威数据之间是否满足业务所需的一致性要求。

理解这一点,比单纯记住"穿透、击穿、雪崩"的三个定义更重要。

Redis 部分到这里就完成了。

下一篇会进入 Kafka,从 Broker、Topic、Partition、Offset 和 Consumer Group 开始,理解消息为什么能够被多个服务异步消费,以及 Kafka 如何分配消费者的工作。

相关推荐
tongyiixiaohuang1 小时前
物料成本核算单同步返回信息策略实战教程:从金蝶云星空回写 MySQL 的闭环设计
数据库·mysql
Qianxueban1 小时前
pexpire key底层逻辑是如何实现这个延迟功能?
redis
星云低代码开发平台1 小时前
AI 工作台记录删除与业务审计留存:三层数据边界设计
数据库·人工智能·架构
code_slave(码畜)2 小时前
微服务架构落地:分库分表实战(下) —— 分布式主键、跨分片查询与分页聚合防坑
数据库·spring boot·分布式·spring cloud·微服务·架构·分库分表
dishugj2 小时前
https://blog.csdn.net/dishugj/article/details/162068579
数据库·系统架构
时间售卖铺2 小时前
MySQL 安装部署
运维·数据库·mysql·云原生
泡海椒2 小时前
JQuick-SQL 子查询:WHERE、SELECT、HAVING 与派生表实践
数据库·sql
用户0510122572962 小时前
SQLite 学习博客(一):从零建库,理解 SQLite 的角色与第一张表
数据库·sqlite
福娃筱欢2 小时前
金仓数据库配置SSL双向认证
数据库·网络协议·ssl