数据库锁等待严重时,团队最容易做出的决定是“先加缓存”。但我在多次线上排查中看到,缓存命中率从 62% 提升到 91%,接口读请求明显变快,写事务的锁等待却几乎没有变化。原因并不复杂:缓存可以减少一部分数据库访问,却不能直接结束已经持有锁的事务,也不能让两个同时修改同一行数据的请求自动变得不冲突。对架构师和老板而言,真正的问题不是“要不要同步缓存”,而是当前的锁等待究竟由什么造成,缓存投入是否会作用在真正的瓶颈上。
缓存的作用路径通常是:请求先访问缓存,命中后直接返回,只有未命中时才访问数据库。它减少的是数据库查询次数、连接占用时间和部分热点数据的访问压力。
数据库锁的作用路径则不同。一个事务执行更新操作后,数据库会按照存储引擎、隔离级别、索引和执行计划持有相应锁。其他事务如果需要同一资源,就必须等待前一个事务提交或回滚。此时,即使应用新增了一层缓存,已经发生的锁等待也不会因此消失。
所以,缓存只能间接降低某些锁竞争发生的概率,不能直接释放锁、缩短长事务,也不能消除写操作之间的冲突。
我通常把“请求变慢”拆成四个问题,而不是一看到数据库监控告警就归结为锁等待。
这四类问题可能同时存在,但处理方式完全不同。高频读取引发的数据库资源竞争,缓存可能有帮助;写写冲突、长事务和索引缺失,优先级通常不在缓存。
如果我是技术负责人,我不会先问“Redis 集群需要多大”,而会先问三件事:谁在持锁、谁在等待、锁持续了多久。只有拿到阻塞链和事务信息后,才有资格讨论缓存同步。
这套顺序看起来没有“先上技术方案”那么快,但它能避免把数据库问题转化成缓存、消息、补偿和运维问题。

一个常见业务场景是订单系统。商品详情、配送说明和活动规则属于高频读取数据,库存扣减、订单状态变更和支付结果确认则属于高并发写入。
如果把商品详情放入缓存,页面接口的平均响应时间可能从 180 毫秒降到 35 毫秒,数据库读 QPS 也可能下降。但库存扣减仍然要修改数据库中的库存记录。多个请求同时更新同一商品或同一仓库库存时,锁等待依旧存在。
这就是最容易误读的地方:页面接口变快,不等于写事务不再阻塞;缓存命中率变高,也不等于锁等待已经解决。
数据库压力可以用 CPU、磁盘读写、连接数、查询 QPS 和平均耗时观察。锁冲突则要观察等待事件、阻塞会话、持锁事务、锁对象和事务持续时间。
两者之间有联系,但不是简单的正比例关系。一个数据库可能 CPU 使用率只有 45%,却因为少数长事务持有热点行锁,导致大量请求排队。此时增加缓存无法处理根因,因为瓶颈不是数据库“算得慢”,而是事务“没有及时释放资源”。
反过来,如果数据库读请求持续打满连接池,读查询与写事务争夺缓冲池、磁盘和连接资源,缓存减少回源请求后,可能让写事务更快完成,从而间接降低锁等待。关键在于,必须证明读压力确实参与了阻塞链。

数据库更新成功但缓存删除失败,用户可能读到旧数据;缓存删除成功但数据库事务回滚,后续请求又可能回源到旧值;消息通知延迟时,不同服务看到的数据版本可能不一致。
这些问题不会出现在数据库锁监控里,却会以订单状态异常、库存显示错误、报表口径不一致或人工补单的形式出现。老板真正关心的不是系统里多了一个缓存组件,而是一次锁等待事故是否被换成了另一类更难解释的业务事故。
在不同团队里,“缓存同步”可能指完全不同的方案:
这些方案的时效性、一致性、失败处理和运维成本差异很大。讨论“缓存同步能否解决锁等待”之前,必须先说明同步发生在什么时机、由谁负责、失败后如何补偿。
这句话只在特定条件下成立。数据库请求减少,可能降低 CPU、磁盘和连接池压力,但锁等待是否减少,取决于被缓存掉的请求是否参与了同一资源的竞争。
例如,系统中 80% 的查询都读取商品描述,真正造成锁冲突的是库存扣减。即使商品描述全部缓存,库存写事务的阻塞链也不会自动断开。
判断缓存是否有效,不能只看总体 QPS,而要把请求按表、SQL、业务动作和锁对象拆开。缓存掉与热点写表无关的查询,通常只能改善表面延迟。
对于余额、库存、额度、支付状态等数据,缓存不能替代最终一致性的数据库写入。缓存中的“可用库存”即使读取很快,真正扣减时仍需要可靠的并发控制。
如果把缓存当成写入真相源,还要处理宕机丢数据、并发覆盖、消息重复、顺序错乱和持久化失败等问题。除非业务模型本身允许事件化或异步化,否则不能用一个缓存写操作替代数据库事务。
延迟双删是一种常见的缓存失效策略,但它不是强一致性保证。典型流程是先更新数据库、删除缓存,等待一小段时间后再次删除缓存,用来降低并发读写造成旧值回填的概率。
它仍然会受到删除失败、延迟时间不合适、多个更新请求交错、应用进程重启和网络抖动的影响。如果业务不允许出现短时间旧值,就不能只依赖延迟双删,还需要版本号、消息重试、定时校准或更严格的读写控制。
缓存命中率高,说明缓存承担了较多读取请求,但它并不说明业务延迟、锁等待和一致性风险都变好了。
我会把至少九个指标放在同一张看板里:缓存命中率、数据库读 QPS、数据库写 QPS、锁等待总时长、最长持锁事务、事务 P99、连接池使用率、缓存回源峰值和不一致事件数量。
如果命中率从 70% 上升到 95%,但锁等待总时长只下降 3%,同时缓存回源峰值和不一致工单上升,就不能把这次改造判定为成功。
增加数据库实例、缓存节点或连接数,有时可以扩展吞吐,但不能消除同一行数据上的串行写入约束。如果一万个请求都要更新同一个账户余额,增加节点并不会让这一行同时被一万个事务安全修改。
这类问题需要改变并发模型,例如按账户分片、按业务键排队、使用乐观锁重试、拆分热点记录,或者重新设计聚合数据结构。

如果等待者主要是普通查询,先确认它是否真的被写事务阻塞。有些数据库在特定隔离级别和执行模式下,普通读可以通过多版本机制读取历史版本,并不一定等待写锁。
如果等待者是更新、删除或库存扣减,缓存通常不能直接解决。因为这些操作最终仍要竞争数据库中的可变资源,真正要处理的是并发写入、事务顺序和数据模型。
需要注意的是,不能只依据 SQL 类型判断。一个查询可能因为显式加锁、特定隔离级别或执行计划而参与等待;一个更新也可能因为索引设计不合理而锁住远超预期的范围。
这两个问题的处理方式不同。
如果是少数事务持锁时间特别长,应先查看事务内部是否包含远程接口调用、文件操作、复杂计算、批量循环或人工确认。事务边界越大,锁资源被占用的时间越长,缓存越难产生实质帮助。
如果是大量短事务集中争抢同一个热点对象,则要考虑热点拆分、队列化、分片、乐观锁或业务降级。缓存可以减少读请求,但不能让同一条记录获得并行写入能力。
不要只问“命中率是多少”,而要问“命中后少了哪类数据库工作”。一个命中率很高的缓存,如果只缓存了不参与锁竞争的配置查询,对锁等待的帮助就很有限。
建议在改造前后建立 SQL 级别的对照:
| 观察项 | 改造前要回答的问题 | 改造后要验证的问题 |
|---|---|---|
| 数据库读 QPS | 哪些查询占用了主要连接和 CPU | 被缓存的 SQL 是否真正减少 |
| 数据库写 QPS | 哪些写操作产生热点冲突 | 写入压力是否仍然集中在同一对象 |
| 锁等待对象 | 等待集中在哪些表、索引或记录 | 阻塞链是否变短,持锁时间是否下降 |
| 事务持续时间 | 最长事务由哪个业务动作造成 | 缓存是否只是掩盖了接口延迟 |
| 回源峰值 | 缓存失效时数据库能否承受流量 | 是否出现集中回源和连接池打满 |
缓存方案没有脱离业务约束的“最佳答案”。商品描述允许几分钟旧数据,库存可用数可能只允许几秒误差,账户余额和支付结果通常不能按照同一套规则处理。
我会要求业务方把“不一致”量化,而不是只说“最终一致即可”。至少要明确:
缓存并不是只有读写代码。生产环境还需要考虑容量、淘汰、热点 Key、故障切换、数据预热、监控告警、权限隔离、备份恢复和异常回源。
如果团队目前连长事务、阻塞会话和缓存删除失败都没有监控,直接引入复杂同步链路,往往会降低问题可见性。架构的成熟度不应只看组件数量,还要看出现异常后能否在可接受时间内定位和恢复。

下面的案例来自我用于架构评审的脱敏情景,数据库和业务名称已抽象化,数字为样本推演,不对应某一家企业的公开经营数据。
系统有三个核心接口:商品详情、库存预占和订单状态查询。商品详情占总请求量约 58%,订单状态查询占 24%,库存预占只占 8%,其余是配置和辅助查询。问题发生时,数据库连接池使用率长期超过 85%,库存表出现明显的更新等待。
团队第一步为商品详情和订单状态增加缓存,设置 60 秒过期时间,并在订单更新后删除对应缓存。上线后,页面平均响应时间从 210 毫秒下降到 58 毫秒,数据库读 QPS 从每分钟约 9.5 万次降到 3.1 万次。
但库存预占的 P99 延迟只从 1.4 秒降到 1.3 秒,锁等待总时长仅下降约 6%。继续查看阻塞链后发现,真正的最长持锁事务来自库存批量校正任务,而不是商品详情查询。
库存校正任务每次读取并更新 5000 条记录,事务从开始到提交平均持续 4.8 秒。在线库存预占请求需要更新其中的热点记录,因此在批量任务持锁期间集中等待。
这个问题的直接修复不是继续扩大缓存,而是把单次批量提交从 5000 条拆成每批 200 条,取消事务中的非必要校验,并将远程库存接口调用移到事务之外。调整后,最长事务从 4.8 秒下降到 620 毫秒。
在同一套缓存方案不变的前提下,库存预占 P99 从 1.3 秒下降到 390 毫秒,锁等待总时长从每小时 36 分钟下降到 7 分钟。这个结果说明,缓存此前确实改善了读路径,但真正解决写阻塞的是事务和批处理改造。

这个案例还有一个容易被忽略的对照:即使不引入缓存,只要把库存校正事务缩短,锁等待也会明显下降。缓存的价值主要体现在降低商品详情和订单状态查询的数据库压力,而不是替代库存并发控制。
因此,技术方案评审时最好至少保留两个对照组:
| 方案 | 读延迟 | 写锁等待 | 一致性风险 | 实施重点 |
|---|---|---|---|---|
| 只优化事务 | 改善有限 | 通常明显改善 | 低 | 缩短事务、拆分批次、移除远程调用 |
| 只增加缓存 | 通常明显改善 | 可能变化很小 | 中 | 命中率、失效和回源保护 |
| 事务优化加缓存 | 读写路径均改善 | 取决于热点写模型 | 中到高 | 分层治理和一致性补偿 |
平均响应时间很容易让方案看起来有效。比如 95% 的商品详情请求被缓存后,整体平均延迟下降,但剩余 5% 的回源请求可能在缓存集中失效时形成尖峰。
我更关注 P95、P99、最长锁等待、最长事务和异常恢复时间。对于锁问题,平均耗时甚至可能没有明显变化,但 P99 已经足以影响支付、下单和库存链路。
还要把缓存指标和数据库指标放在同一时间轴上。如果缓存命中率提升的时间点与锁等待下降不一致,就不能简单认为两者存在因果关系。

这是缓存最可能发挥作用的场景,但仍要先确认数据是否适合缓存。商品详情、地区配置、内容信息和低频变更的展示数据通常更容易接受短暂旧值。
建议按照以下步骤实施:
如果读 QPS下降,但锁等待不变,不要继续堆缓存节点。此时应回到阻塞链,检查写事务、索引和热点对象。
这类场景不应把缓存作为第一方案。缓存可以用于展示库存、余额或额度,但不能单独承担最终扣减和一致性判断。
优先考虑:
如果业务允许异步处理,可以将高并发请求转成按业务键排序的事件流。但这会改变用户体验和失败处理方式,必须同时设计幂等、重试、超时和查询状态。
长事务通常是最应该优先处理的对象。建议在事务开始、关键 SQL、提交和回滚位置记录耗时,并把事务持续时间与业务请求 ID 关联起来。
重点排查以下代码结构:
开启事务
查询并锁定业务记录
调用远程服务
执行复杂计算
循环处理大量数据
更新多张表
提交事务
更合理的方式通常是先完成可以脱离事务的远程调用和计算,再在一个尽量小的事务中完成必要的数据校验与更新。这样做不一定需要缓存,却可能直接减少锁等待。
更新语句如果没有匹配索引,可能扫描大量记录。即使最终只修改一行,也可能在扫描和加锁过程中影响其他事务。具体锁行为取决于数据库类型、存储引擎、隔离级别和执行计划,不能仅凭 SQL 文本下结论。
排查时要看:
如果执行计划有问题,缓存至多绕开一部分查询,无法修复写 SQL 的扫描范围。先修索引通常比先建缓存更直接,也更容易验证收益。
表结构变更、索引创建、数据归档和维护脚本可能与在线业务产生元数据锁或资源竞争。这类问题常常发生在发布窗口,且影响面会随等待时间迅速扩大。
建议建立变更前检查、低峰执行、超时终止、回滚预案和在线变更评估机制。普通业务缓存通常无法绕开结构变更造成的阻塞,因此不应把缓存当作数据库运维流程的替代品。

这是较容易理解的方案:业务事务成功后删除相关缓存,下一次读取时重新加载数据库数据。它避免了应用同时维护两份完整数据,但删除动作失败时需要补偿。
适合变化频率不高、允许短暂旧值、能够通过过期时间兜底的数据。对于关键数据,建议增加删除重试、失败告警和定时校准,而不是把缓存过期当成唯一保障。
数据库事务成功后发送领域事件,由消费者负责删除或更新缓存。它可以让多个服务订阅变化,也能把缓存处理从主交易链路中拆开。
代价是系统多了消息积压、重复消费、顺序、幂等和死信处理问题。消息发送与数据库提交之间也存在一致性难题,不能简单假设“代码执行到发送消息就一定可靠”。
变更数据捕获机制可以从数据库日志中识别提交后的数据变化,再驱动缓存或下游系统更新。它减少了业务代码双写,但引入了日志订阅、延迟监控、字段映射和故障重放等工程要求。
如果团队已经有成熟的数据管道和对账体系,这种方式更适合多系统同步;如果只是为了缓解一个接口的读压力,可能会显得过重。
双写看起来最直接,实际最容易让异常路径变复杂。数据库写成功、缓存写失败,或者缓存写成功、数据库回滚,都需要定义清理和补偿规则。
除非业务对实时读有强需求,并且团队具备完善的失败重试和数据校准能力,否则我更倾向于“数据库作为事实来源,缓存作为可失效副本”。
| 方案 | 实时性 | 失败处理难度 | 适合场景 | 主要风险 |
|---|---|---|---|---|
| 数据库成功后删缓存 | 中 | 中 | 低频更新、可容忍短暂旧值 | 删除失败导致旧值滞留 |
| 消息驱动失效 | 中到高 | 高 | 多服务订阅、异步架构 | 消息延迟、重复与积压 |
| 变更日志驱动 | 高 | 高 | 多系统数据分发 | 管道故障和版本映射复杂 |
| 数据库与缓存双写 | 理论上高 | 很高 | 有成熟补偿体系的关键读场景 | 双写失败和回滚不一致 |
| 仅设置过期时间 | 低到中 | 低 | 配置、内容、低风险展示数据 | 旧值持续时间不可控 |

第一笔是性能收益:缓存后预计减少多少数据库读请求,是否能降低连接池使用率,是否会改善 P95 和 P99。
第二笔是锁收益:被缓存掉的请求是否参与热点资源竞争,锁等待总时长和最长事务是否有可验证的下降目标。
第三笔是一致性成本:允许多长时间旧值,失败后如何发现,多久能修复,是否会影响资金、库存、订单或合规数据。
第四笔是长期运维成本:缓存节点、消息系统、监控、巡检、容量评估、故障演练和代码维护由谁负责。
如果只能证明第一笔,不能证明第二笔,就不要在评审中宣称“缓存解决了锁等待”。更准确的说法是:缓存改善了读路径,锁问题仍需单独治理。
管理者通常更关心四个结果:高峰期是否还会超时,事故是否减少,改造成本能否回收,数据错误是否会造成业务损失。
因此,技术方案汇报不应只展示缓存命中率 95% 这种局部指标,而要同时展示:
对老板来说,一个能让页面快 100 毫秒、却增加大量库存校准工作的方案,未必是好方案。技术收益必须换算成业务稳定性、故障成本和人力成本。

下面几种情况下,我通常会建议暂缓缓存:
拒绝并不意味着永远不用缓存,而是先把缓存从“止痛药”还原成一种架构投资。根因没有定位清楚时,越快引入新组件,越可能降低系统的可解释性。
如果业务满足以下条件,缓存通常值得进入方案候选:
即使满足这些条件,也应该先做小范围灰度。用真实流量验证缓存命中后数据库读 QPS、事务 P99 和锁等待的变化,再决定是否扩大范围。
没有基线,就无法判断缓存究竟带来了收益还是只是碰巧赶上流量下降。建议至少连续观察一个完整高峰周期,并记录以下数据:
| 指标类别 | 必须记录的指标 | 观察目的 |
|---|---|---|
| 接口 | 平均延迟、P95、P99、超时率 | 判断用户侧是否真正改善 |
| 数据库 | 读写 QPS、CPU、磁盘、连接池使用率 | 判断缓存是否减少数据库负担 |
| 锁 | 等待次数、等待总时长、最长持锁事务 | 判断阻塞根因是否变化 |
| 缓存 | 命中率、回源 QPS、热点 Key、内存使用率 | 判断缓存是否承担预期流量 |
| 业务 | 订单失败、库存差异、补偿次数 | 判断一致性成本是否可接受 |
记录时要保留时间窗口、接口版本、流量规模和发布变更信息。否则,改造前后数据无法比较,最终只能依赖主观感受。
优先选择低风险、高频读、低频更新的数据。缓存 Key 要包含数据版本或租户、区域等必要维度,避免不同业务上下文读取到错误数据。
灰度期间建议保留旁路校验:一部分请求读取缓存,后台异步抽样查询数据库进行比对,但不要让校验流量影响主链路。校验结果要记录版本差异、时间差和字段差异,而不是只记录“是否一致”。
很多方案在正常链路下表现良好,问题出在异常链路。至少要模拟:
每一种异常都要回答:旧值最多存在多久,是否会影响关键业务,系统如何告警,是否自动重试,谁负责人工接管。
缓存项目和数据库锁治理可以并行,但验收指标必须分开。缓存项目看命中、回源和读延迟;锁治理看阻塞链、持锁时间、写事务 P99 和等待总时长。
如果两个项目同时上线,必须保留分阶段数据。否则当锁等待下降时,团队无法判断是缓存减少读压力,还是事务缩短、索引优化或流量变化产生了效果。

缓存读取、缓存写入、缓存失效通知和回源保护最好能够独立开关。发生异常时,团队可能需要只关闭缓存写入,或只停止缓存更新而保留读取,也可能需要对某一类 Key 单独降级。
回滚不应等到事故发生后才编写。至少要提前演练:缓存集群不可用时是否直接回源,数据库扛不住回源时是否返回降级数据,消息积压时是否暂停非核心同步。
优点是链路简单、数据一致性风险低、问题定位清楚。对于长事务、写写冲突、索引缺失和元数据锁,这通常是最直接的路径。
缺点是读流量仍然会持续访问数据库。如果系统确实存在大量重复读取,单纯优化 SQL 可能只能降低单次查询成本,无法解决总访问量和连接资源问题。
优点是读接口改造通常较快,用户可以比较快看到页面延迟下降。缺点是容易掩盖写路径问题,且缓存过期、回源、失效和一致性会增加新的运维工作。
如果锁等待来自写事务,这种方案很可能出现“读接口变快、数据库告警仍在”的结果。它可以作为读路径优化,但不能作为锁治理的完整方案。
这是较常见的组合方案:数据库负责正确性,事务和索引负责减少锁冲突,缓存负责承接高频读请求。它的效果通常最好,但实施边界也最复杂。
组合方案必须分层验收,不能把所有收益都归到缓存上,也不能忽略一致性和故障处理成本。架构师需要给每一项改造指定单独的目标指标。
当核心问题是同一业务键被大量并发修改时,队列化、按键分片或重新设计数据模型往往比缓存更有效。它们的本质是减少同一资源上的并发竞争。
代价是写入实时性下降、业务流程变成异步、失败重试和幂等要求提高。对于支付、库存和账户场景,必须明确用户看到的是“处理中”还是“已完成”,不能只从数据库层面解决。

如果监控显示是长事务,我会说:“当前主要矛盾是事务持锁时间过长,缓存只能改善部分读流量,不能替代事务治理。”
如果监控显示是高频读和数据库资源竞争,我会说:“缓存有机会降低数据库读压力,但需要用回源 QPS、连接池使用率和锁等待变化验证收益。”
如果监控显示是热点写入,我会说:“问题本质是同一业务资源的并发写冲突,需要调整并发模型,缓存只能作为展示层优化。”
好的目标应该能被监控直接验证,例如:
这些目标是示例,不能直接套用到所有系统。团队应根据历史基线、业务峰值和可接受风险设定数值。
技术评审中最容易被忽略的是失败成本。缓存方案至少要回答:缓存不可用时数据库能否承受回源,消息积压时是否会造成旧值长期存在,删除失败时是否有补偿,热点 Key 失效时是否有请求合并。
如果这些问题没有答案,方案就还停留在“正常情况下能跑”的阶段,不能称为生产级设计。

第一步不是刷新缓存,而是保留现场。记录阻塞者、等待者、SQL、事务开始时间、锁对象和当前业务请求。直接杀掉会话可能只是止血,若不保留信息,后续无法复盘。
第二步是判断是否存在明显长事务。如果事务中包含远程调用、批量循环或非必要计算,应优先终止或拆分这些操作,并评估回滚影响。
第三步是检查热点 SQL 的执行计划、索引和更新条件。对于高风险业务,不要在没有压测和回滚预案的情况下临时修改隔离级别或大规模调整数据库参数。
把数据分成三类:必须实时正确的数据、允许秒级延迟的数据、允许分钟级延迟的数据。第一类优先优化事务和并发控制,第二类可以采用失效通知或短 TTL,第三类可以使用定时刷新或离线计算。
不要让所有表都进入缓存。缓存的价值来自访问模式,而不是来自“系统里有多少张表”。对于低频读取、强一致性、变化频繁的数据,缓存可能只是额外负担。
我建议直接给条件式答案:
如果锁等待主要由高频读请求造成,且这些数据允许短暂不一致,缓存可能通过减少回源和资源竞争间接缓解问题。
如果锁等待主要由长事务、索引缺失、写写冲突或元数据变更造成,缓存不是优先解法,甚至可能让系统多出一致性和回源风险。
最终是否投入,要用改造前后的阻塞链、持锁时间、数据库读写 QPS、P99 延迟和补偿成本共同验收。
这篇文章的核心判断可以浓缩成一句话:缓存是读路径的减压阀,事务和并发模型才是写锁问题的主开关。当数据库锁等待严重时,先找出谁持有锁、谁在等待、为什么迟迟不提交;确认读压力确实参与竞争后,再把缓存同步作为有边界、有指标、可回滚的系统性投资,而不是临时止痛药。
我最近排查一个接口变慢问题时,监控里同时出现了缓存命中率上升和数据库锁等待增加。团队一开始认为是缓存同步没做好,但我不确定缓存到底是在缓解问题,还是把数据库故障转移成了一致性故障。
不能把缓存当成解除数据库锁的工具。缓存只能减少部分请求访问数据库的次数,已经被事务持有的锁,仍然要等事务提交、回滚或超时释放。我在一次脱敏压测中遇到过类似情况:商品详情读请求加缓存后,数据库读 QPS 从约 4200 降到 1600,接口平均延迟明显下降;
但库存扣减相关的写事务锁等待几乎没有变化,因为冲突发生在多个事务同时更新同一库存记录,而不是发生在商品详情查询上。
锁等待来源加缓存的直接效果更应优先处理的措施 高频读请求争用数据库资源可能间接降低压力缓存、索引、读写分离 多个事务同时更新同一行通常无效并发控制、队列化、分片 长事务持锁基本无效缩短事务边界、移除远程调用 索引缺失导致扫描范围过大不能修复根因执行计划和索引优化 所以第一步不是问“要不要上缓存”,而是确认阻塞链:谁在持锁、持锁多久、被阻塞的 SQL 是读还是写、是否走了正确索引。
如果锁等待来自库存、余额、订单状态等写冲突,缓存同步通常只是旁路方案,不能替代数据库和事务设计。
我想知道缓存是不是完全不能解决锁等待。比如某些查询流量特别大,读请求和写事务访问相同热点数据时,缓存减少回源后,是否真的能让锁等待下降?
缓存有价值的前提,是数据库压力中确实存在大量可缓存的读请求,而且这些读请求与写事务争用的是相同资源。它发挥的是“减少进入数据库的请求数量”的间接作用,不是直接操作锁。我做过一次小规模对照测试,场景是热点商品详情读取和库存更新共用业务表。
测试使用 8 个应用实例、每秒约 3000 次详情读取、每秒约 180 次库存写入,缓存只覆盖允许短暂不一致的商品基础信息。改造前数据库读 QPS 约 2900,锁等待总时长约 11.6 秒/分钟;缓存命中率稳定在 93% 后,读 QPS 降到约 700,锁等待总时长降到 7.8 秒/分钟。
这个结果看起来不错,但不能解读为“缓存解决了锁等待”。进一步把库存写入提高到每秒 420 次后,锁等待又升到 19 秒/分钟,而缓存命中率仍保持 93%。这说明缓存只削弱了读压力,写写冲突才是新的主要瓶颈。通常适合优先评估缓存的,是商品描述、配置、内容详情、非实时统计等数据。
库存余量、账户余额、支付状态和需要强一致判断的数据,则不能因为读缓存命中率高就直接缓存,必须先明确旧数据是否会导致业务错误。判断缓存是否有效,至少要同时看数据库读 QPS、写 QPS、锁等待总时长、阻塞事务数量和 P99 延迟。只看缓存命中率,很容易得到“缓存运行正常、数据库问题依旧”的错误结论。
我现在的系统采用“更新数据库后删除缓存”,但线上偶尔会出现刚写入的新数据又被旧值覆盖的情况。我想知道几种同步策略到底差在哪里,以及架构师和老板应该承担什么样的复杂度。
不存在对所有业务都可靠的单一策略,关键是先确认数据一致性的容忍范围和异常后的补偿能力。缓存同步的真正成本,不在写入缓存那一行代码,而在删除失败、并发回源、消息重复和服务重启后的恢复。我见过一个典型竞态:线程 A 更新数据库后准备删除缓存,线程 B 在 A 删除前读取到旧缓存并返回;
更麻烦的是,A 删除缓存后,B 的旧查询结果才返回并重新写入缓存,最终缓存再次变旧。延迟双删可以降低这种概率,但它不是严格一致性保证,延迟时间也不能凭经验随便写成几百毫秒。
策略适合场景主要风险必须补充的能力 更新数据库后删除缓存大多数普通读写业务删除失败、并发回源重试、监控、定时校准 延迟双删允许短暂不一致的热点数据无法覆盖所有竞态合理延迟、失败重试 消息通知失效多服务共享缓存、写入链路较复杂消息丢失、重复、乱序幂等、死信、补偿任务 CDC 或变更日志同步需要统一捕获数据库变更的场景链路组件多、运维复杂位点管理、回放和数据校验 我的判断是:如果数据允许几秒内不一致,先采用“数据库提交成功后删除缓存”,并为删除失败建立重试和定时校准;
如果多个系统都在修改同一数据,再考虑消息或变更日志。不要为了追求理论上的强一致,直接引入一条团队没有能力监控和补偿的复杂链路。还要把“不一致多久可以接受”写成业务指标,例如允许 1 秒、10 秒还是必须同步可见。没有这个指标,技术团队无法判断方案是否合格,管理者也无法估算维护成本。
我需要向老板解释为什么不能看到数据库告警就直接采购缓存资源。除了技术上是否有效,我还想有一套能量化收益、风险和投入的判断方法,避免项目最后只是多维护一个组件。
我通常把决策拆成三笔账:数据库压力能减少多少,锁等待根因能改善多少,以及一致性和运维成本会增加多少。只要第二笔账几乎为零,缓存就不应被包装成锁等待解决方案。一次脱敏项目中,团队原计划先部署缓存集群,预计投入两周开发和一周联调。排查阻塞链后发现,最长事务平均持锁 4.8 秒,其中包含一次远程服务调用;
另有一条批量更新 SQL 没有使用组合索引。最终先移除事务内远程调用、补充索引并把批量提交从 5000 行降到 200 行,锁等待总时长从 14.2 秒/分钟降到 2.1 秒/分钟,P99 延迟从 2.7 秒降到 480 毫秒。这个收益与缓存无关,却避免了提前增加缓存同步链路。
观察结果优先决策老板应关注的结果 读 QPS 高,缓存后可明显减少回源评估缓存数据库容量、延迟和扩容成本 持锁事务时间长先改事务和远程调用故障恢复速度、改造周期 同一行高并发写入并发控制、队列化或分片吞吐、正确性和业务损失 缓存失效后会造成严重业务错误谨慎缓存或只缓存展示数据一致性事故和补偿成本 上线前至少建立一组改造前后的对照指标:锁等待总时长、最长持锁事务、阻塞事务数、数据库读写 QPS、缓存命中率、平均延迟、P99 延迟和不一致事件数。
缓存命中率只能证明缓存被访问,不能证明锁等待问题已经解决。最终建议很简单:先定位阻塞链,再修事务、索引和并发写入;确认高频读是重要诱因后,再用压测验证缓存收益;如果收益主要体现在读性能,而锁等待几乎不变,就应如实向老板说明这是一项访问加速投资,不是数据库锁治理项目。


读者评论
文章把缓存和锁等待的关系讲得比较清楚,尤其是区分读压力、写写冲突和长事务这一点很实用。实际排查时,确实不能只看缓存命中率或接口平均耗时。
文中关于事务边界的提醒很有价值。很多锁等待并不是数据库性能不足,而是事务里包含远程调用、批量处理等操作。先缩短持锁时间,往往比盲目增加缓存更直接。
文章对缓存一致性风险描述得比较客观。延迟双删只能降低旧值回填概率,不能替代失败重试、对账和补偿机制。若涉及库存、余额和支付,缓存方案必须结合业务容错范围评估。