Java

缓存层出问题后,为什么应用和数据库会一起变慢的等待型链路?一次 Redis 客户端超时引发的连锁故障

缓存层故障最怕的不是少了一层加速,而是请求持有时间被一层层拉长。这次事故里,先变坏的是 Redis 客户端超时,随后才是数据库回源、连接池等待和线程排队,一条完整的等待链就是这样被拖出来的。

  • Redis
  • 缓存
  • 数据库
  • 线程池
  • 稳定性
10 分钟阅读

缓存层出问题以后,很多人的第一反应还是那句老话:大不了退化直连数据库。听起来像是少一层加速器,系统只是慢一点。

可真实事故里,最麻烦的从来不是“多查一次数据库”,而是请求会在每一层都多停一会儿。一旦持有时间被拉长,连接、线程、重试就会跟着一层层堆起来。

上次支付确认链路出事,就是很典型的一次。最开始看到的是应用 RT 抖、数据库连接池变紧、只读库查询上涨,现场看上去像三层一起坏了。可把时间线重新拉直后会发现,第一张倒下的牌其实是 Redis 客户端超时。

第一波异常不是 miss,而是 Redis RT 先抬头

那天 19:42 开始,支付确认接口 confirmPay 的 p99 从 240ms 抬到 1.6s。很多人先去看数据库,因为只读库 QPS 和连接池 active 都在涨。

但把时间线往前拽 2 分钟,最早变坏的是这几条:

  • Redis 客户端 p95 从 8ms 抬到 160ms
  • 客户端超时数开始增加
  • 业务日志里 get cache timeout, fallback to db 连续出现

注意,这时候命中率还没有明显掉。也就是说,问题一开始不是“大量 miss”,而是请求先卡在查缓存这一步

为什么应用和数据库会一起慢下来

因为一次请求的持有时间被拆成了两段等待。

正常时,支付确认链路会先读 Redis 里的用户风控快照和支付路由配置,几毫秒就结束;现在 Redis 先卡 100 多毫秒,失败后还要继续去数据库回源。

这会产生一个很糟糕的连锁:

  • 线程先在 Redis 客户端上等一段
  • 超时后再去申请数据库连接
  • 数据库查询本身变多,连接占用又变长
  • 后面的请求因为拿不到连接和线程,再继续排队

于是现场看起来就像数据库和应用一起变慢了,可它们其实是在接同一段被拉长的等待。

这次事故真正的证据链是什么

后来把监控按分钟摊开,顺序非常清楚:

  1. 19:42 Redis 客户端 RT 抬头,超时数上升
  2. 19:43 fallback to db 日志增多,只读库查询量开始上升
  3. 19:44 Hikari 活跃连接接近上限,获取连接耗时变长
  4. 19:45 业务线程池 queue 开始堆积,接口 p99 再次抬高
  5. 19:46 网关出现重试,请求总量被进一步放大

你会发现,真正一层层把系统拖慢的,不是某个组件突然坏死,而是等待在传导。

根因不是数据库先顶不住,而是 Redis 客户端连接池被打穿

继续查 Redis 侧,服务器本身没有明显故障,节点 RT 也还行。真正出问题的是应用侧 Redis 客户端连接池配置太紧,叠加那一波流量峰值后,连接借用等待开始抬高。

换句话说,最早的缓存故障甚至不在 Redis 服务端,而在客户端拿连接这一步。

这也是这类事故最容易误判的地方:大家看到数据库查询变多、线程池排队,就很自然地往下游找;可如果上游缓存访问本身已经变成慢动作,后面的每一层都会跟着变形。

为什么这次止血先压的是等待,而不是先追根因

当时最先做的几个动作都很“土”,但很有效:

  • 临时把 Redis 客户端超时从 200ms 降到 80ms,避免线程长时间吊死在缓存上
  • 关掉支付确认链路上的一次自动重试
  • 对风控快照加本地 30 秒兜底,减少回源数据库的比例

动作下去以后,回落顺序也很典型:

  • 线程池 queue 先停住不再继续涨
  • 数据库连接池 active 开始回落
  • 只读库 QPS 和接口 p99 才慢慢降下来

这一步特别说明问题:救场时最该先打断的,不是抽象地“恢复缓存”,而是把整条等待链里的持有时间压短。

为什么“缓存坏了就直连数据库”是句危险的话

因为这句话默认了一个过于理想的前提:数据库和应用还有足够余量,足够把多出来的等待吞掉。

可真实线上里,只要你本来就在高峰,或者确认链路本来就重,缓存访问哪怕只多卡几十毫秒,请求持有时间都会明显变长。后面的数据库连接池、业务线程池、网关重试,就会像多米诺骨牌一样一张张倒下。

所以缓存层故障真正危险的地方,不是失去加速,而是它把整条链路都拖进了“资源释放变慢”的状态。

这类现场到底先看什么

如果你看到应用和数据库一起慢,而缓存又有异常,不妨先把下面几件事放到同一条时间线上:

  • Redis 客户端 RT 和超时
  • fallback to db 或 miss 后回源日志
  • 数据库连接池 active / pending
  • 业务线程池 queue
  • 网关或 RPC 重试量

只要顺序能对上,你通常就能分清:到底是数据库自己先慢,还是缓存把等待一路传下去了。

什么时候这篇思路最有用

当你现场长成下面这样时,这条等待链最值得先看:

  • Redis 指标或客户端日志先有抖动
  • 数据库和应用几分钟后一起恶化
  • 命中率不一定先崩,但回源和排队都在变多
  • 超时、重试、连接池等待会一层层接上来

这种形状和单纯 TTL 失效、热 key 打穿、key 设计退化都不太一样。它更像缓存访问先慢了,然后整条业务链都被拖长。

回到这次事故最该记住的一句

缓存层出问题后,应用和数据库会一起变慢,很多时候不是因为它们各自都坏了,而是同一批请求在每一层都多等了一会儿。

所以排查这种故障时,我现在最先看的已经不是“哪个组件更红”,而是:谁先开始拉长请求持有时间。 一旦这一步看清,止血动作也会更直接——先把等待截断,再谈更细的根因修复。