本文目录
可直接引用的结论:热点行不是 SQL 写得慢,而是很多正确请求必须争用同一个提交点
本文面向维护账户额度、活动计数、任务状态、租户配额或设备游标的研发、数据库与运维负责人。先用业务键、事务时间线、等待事件和阻塞关系确认竞争是否集中在同一行、索引范围或固定顺序;再把外部调用、批量读取和无关计算移出事务,以单条受约束更新表达能够原子完成的规则。若业务允许拆分,才采用分桶、追加流水、按键排队或异步聚合,并明确读一致性、总额约束、失败恢复和对账。验收必须使用真实热点分布,比较有效完成量、锁等待、事务失败、重试次数和最终业务不变量,不能用均匀随机数据或平均延迟证明热点已经消失。
不同热点写入,需要不同的正确性边界
| 热点类型 | 优先验证的方案 | 容易忽略的风险 |
|---|---|---|
| 单行状态转换 | 用当前状态或版本作为条件,一条更新完成校验与转换,并检查受影响行数 | 先查后改留下竞争窗口,失败请求被误当作成功 |
| 可合并计数 | 评估分桶或追加增量,读取时聚合,定期校准权威总数 | 拆分后读取成本、陈旧窗口和总上限失去定义 |
| 严格余额或额度 | 由权威账本与数据库约束保护不变量,缩短事务并按稳定顺序更新 | 为了吞吐拆散原子边界,出现透支、重复记账或无法对账 |
| 可排队后台任务 | 按业务键进入有界队列或领取表,限制同一资源并行并支持恢复 | 把锁等待搬成无限积压,领取超时后出现双执行 |
适用边界:热点行与慢查询、连接不足不是同一个问题
热点行是许多并发事务需要修改或锁定同一个逻辑资源,后来的事务即使 SQL 很短,也只能等待先前持有者提交或回滚。它常见于全局计数、单租户额度、同一账户余额、任务游标和唯一状态行;普通查询扫描慢、磁盘饱和或连接池排队,需要先按各自证据处理。
同一接口变慢不代表一定存在行锁。先把应用排队、连接获取、SQL 执行、锁等待、提交和响应拆开,查看等待是否随特定业务键集中。PostgreSQL 官方文档说明,行级锁不会阻止普通读取,但会阻塞对同一行冲突的写入或加锁操作,并在事务结束时释放;MySQL InnoDB 的具体记录锁、间隙锁和锁定读行为还取决于索引、搜索条件与隔离级别。
若写入均匀分散却仍出现吞吐上限,应继续检查索引维护、日志刷盘、复制确认、缓冲争用或存储能力。若热点来自一个真实且不可分割的业务事实,例如严格账户余额,就不能仅凭技术目标拆成多个互不约束的数值;优化目标应是减少不必要持锁时间、控制等待和重试,并在明确一致性边界内提高完成量。
- 记录数据库产品、版本、隔离级别、事务框架和自动重试配置。
- 按业务键比较请求量、锁等待、事务时长、失败与提交量。
- 写清热点字段代表的业务事实,以及哪些不变量绝不能拆开。
先还原阻塞链:找出持锁者、等待者与临界区
诊断要回答“谁持有什么锁、从何时开始、谁被阻塞、事务里还做了什么”,而不是只保存一条慢 SQL。将数据库会话、锁视图、应用追踪和业务单据按同一时间线关联,区分头部阻塞者与其后排队请求。
PostgreSQL 可结合 pg_locks、pg_stat_activity 及阻塞关系函数查看未授予锁、事务开始时间、等待事件和阻塞会话;官方文档明确 pg_locks 可用于定位未授予锁较多的关系并评估争用随流量的变化。MySQL 可通过 Performance Schema 的事务与数据锁等待信息、死锁记录和语句上下文定位双方。采集需要受权限、频率和敏感字段控制,避免为了诊断持续运行高成本全量查询。
重点寻找事务取得热点锁后进行的网络请求、文件操作、模板渲染、循环处理、人工等待或大范围查询。应用显示请求已超时,不代表数据库事务已经取消;若后台仍继续并最终提交,上游自动重试会把一份业务意图变成更多等待者。阻塞树中大量等待 SQL 可能只是受害者,真正要修改的是最前方持锁事务及其上游调用路径。
- 为样本保存 traceId、业务键、连接会话、事务起止与最终提交结果。
- 分别统计锁获取等待和获得锁后的持有时间。
- 确认应用超时、数据库取消和事务回滚能够对应,不留下后台孤儿工作。
能原子完成的规则,优先改成受约束的单条更新
对“仅当状态为待处理才完成”“仅当余量足够才扣减”等单行规则,可将前置读取和写入合并为带条件的更新,由数据库在同一操作中判断,并以受影响行数或返回值区分成功、条件不满足和系统失败。
先读取值、在应用计算、再无条件写回,会扩大事务往返和丢失更新风险。单条条件更新仍会争用同一行,但减少了一次读取和应用停顿,也让失败语义更清楚。版本列的乐观并发适合冲突较少且重试成本可控的状态修改;在真正热点上,大量版本冲突与立即重试可能比短暂排队更昂贵,因此必须按净完成量选择,而不是默认“无锁”就更快。
多行或多表不变量不能被随意压成几条互不关联的更新。必须更新多个资源时,所有入口采用稳定顺序,事务只包含必要读写,并对死锁或序列化失败执行有上限、带抖动且保持业务幂等的整笔事务重试。PostgreSQL 与 MySQL 都可能在行级操作中发生死锁;数据库回滚一个参与者是恢复机制,不代表业务已经完成。
- 将允许前态、边界条件和唯一业务标识固化为更新条件或数据库约束。
- 把条件不满足、锁超时、死锁、序列化失败和结果未知映射为不同处理路径。
- 重试整笔事务并复用原业务意图,不只重放最后一条 SQL。
缩短临界区:锁住之前准备,提交之后调用外部系统
热点资源一旦被锁定,事务里的每个额外步骤都会转化为其他请求的等待。先完成不依赖锁的校验、数据准备和纯计算,再开启短事务读取必要状态、写入结果与待发布意图并提交;短信、支付、对象存储和 AI 工具调用放到提交后的可恢复流程。
移出事务不等于忽略失败。跨系统动作使用稳定 operationId 或事件 ID,保存待执行、已发送、结果未知、成功和终止失败状态,由幂等执行器查询与补偿。用户端展示“已受理”还是“已完成”必须对应真实提交点。对小程序、APP 弱网补发和 AI 智能体重复工具请求,服务端按同一业务意图复用结果,不能让每次重试都再次争抢热点行。
锁等待和语句超时应短于调用方总截止时间,并为数据库取消、连接归还和上游响应预留预算。超时不是吞吐优化:设置过短只会制造更多回滚和补发。若允许排队,应明确最大等待、队列长度、过载拒绝与稍后查询;若不允许等待,应快速返回可解释的冲突结果,而不是让连接长期占用。
- 事务内不执行不可控网络调用、长循环或与热点事实无关的查询。
- 提交后动作具有持久状态、稳定幂等标识和结果查询入口。
- 端到端超时预算覆盖排队、锁等待、执行、提交与响应,不产生无界重试。
确需拆热点时,用业务语义选择分桶、流水或按键排队
当单个提交点已经成为经证实的容量上限,且业务允许改变读写模型,可在受控范围内拆分写入。可交换、可合并的统计量可按租户、时间窗或固定桶写入,再异步聚合;需要审计的余额变化可追加不可变流水并维护受约束余额;同一资源必须串行的后台动作可按业务键路由到有限分区。
分桶数量不能只追求写入均匀。读取必须说明是强一致总数、允许延迟的展示值还是近似指标;扩桶与缩桶需要版本、路由和历史数据方案。严格全局上限若仍要在每次写入时检查一个总计数器,热点并未消失;若把额度预分配给多个桶,则要定义分配、耗尽、回收、故障占用和总额对账,避免局部都合法但全局超限。
领取型工作队列可在数据库支持且语义匹配时使用跳过已锁记录的能力,让多个执行器处理不同任务;这类读取提供的是队列式并行,不适合要求一致视图的一般业务查询。领取记录还需租约或状态版本、执行幂等、过期接管和陈旧执行者拒绝。无论采用缓存、消息队列还是数据库分片,都不能仅把同一热点键复制到另一个组件后宣称问题解决。
- 为新模型定义权威事实、读一致性、聚合延迟、路由版本和对账公式。
- 以单一热点键验证写入是否真正分散,而不是只看全库平均值。
- 准备停写、排空、校准和回退方案,避免双写期间两个权威来源。
灰度与验收:同时证明等待下降和业务结果不变
先在可复现环境构造与生产接近的热点比例、事务组合和数据规模,保存优化前阻塞链与业务对账;再依次上线事务缩短、条件更新、等待控制和必要的数据模型变更。每一步只扩大一个低风险业务范围,并保留旧路径拒绝新写、暂停异步执行和校准数据的开关。
性能验收同时记录到达量、有效提交量、P95/P99 端到端延迟、锁等待分布、热点行持有时间、死锁或序列化失败、回滚、应用重试、连接占用和队列年龄。使用均匀随机键会隐藏同一行串行上限;只看数据库 QPS 可能把失败与重复也算作能力。压力结束后还要观察积压清空和资源归还,确认恢复阶段没有重试风暴。
正确性验收覆盖同一业务意图并发提交、持锁者进程退出、提交回执丢失、数据库切换、死锁回滚、异步执行器重复和聚合延迟。按业务键对账请求、事务结果、流水、聚合值和外部副作用;任何结果未知都进入可查询与人工处置状态。若吞吐提升依赖放宽一致性,必须由业务明确接受可见延迟和纠正方式,不能把错误窗口隐藏在性能报告里。
- 压测包含单热点、少数热点与真实长尾分布,并固定资源和成功口径。
- 锁等待下降时,有效业务提交量同步提高且重试放大受控。
- 余额、额度、计数、状态与外部副作用均能按稳定业务键完成对账。
需求讨论中的问题
给热点表加索引,就能解决行锁等待吗?
不一定。缺少合适索引可能扩大扫描与锁定范围,因此必须检查执行计划和实际锁对象;但如果所有请求本来就更新同一条记录,再好的索引也不能让冲突写入同时提交。此时应缩短事务、控制等待,或在业务允许时改变写入模型。
把悲观锁改成乐观锁,吞吐一定更高吗?
不一定。乐观版本校验适合冲突较少、计算可重做的场景;同一行持续高冲突时,大量失败与重试会消耗更多数据库和应用资源。应比较有效完成量、冲突率、重试成本和业务失败语义。
可以用 SKIP LOCKED 跳过热点行吗?
它适合多个执行器从工作集合领取不同任务,返回的是当下未被锁住的记录,不适合需要完整一致结果的普通查询,也不能自动解决同一业务键的总额约束。必须结合状态版本、幂等和过期接管设计。
死锁是不是数据库故障?
死锁是并发事务形成循环等待,数据库通常会回滚其中一个事务解除循环。应用仍需识别错误、复用同一业务意图并有上限地重试;若频繁发生,应统一资源访问顺序、缩短事务并检查索引与锁范围。
小程序、APP 或 AI 智能体重复请求怎样避免放大热点?
入口为一次业务意图生成稳定幂等标识,服务端保存并复用处理中或已完成结果;客户端查询权威状态,不因超时创建新意图。外部工具调用也沿用同一标识和截止时间,避免一次结果未知演变成多笔并发更新。
数据库热点行与锁争用上线验收清单
可逐项勾选,辅助本次阅读;刷新页面后不保留。
参考资料
技术概念可进一步阅读以下资料;文中的业务场景与实施清单用于方案讨论。
- PostgreSQL 18 Documentation · Explicit Locking
核验表级与行级锁冲突、行锁释放时点、死锁检测、稳定加锁顺序和避免长事务等待的官方边界。
- PostgreSQL 18 Documentation · Viewing Locks
核验使用 pg_locks 观察未授予锁、定位可能存在争用的关系并评估锁竞争随流量变化的用途。
- MySQL 8.4 Reference Manual · Locking Reads
核验 InnoDB 锁定读、NOWAIT 与 SKIP LOCKED 的语义和不适合一般一致性查询的边界;实际支持需匹配部署版本。
- MySQL 8.4 Reference Manual · How to Minimize and Handle Deadlocks
核验 InnoDB 死锁处理、短事务、统一操作顺序、索引与应用事务重试等官方建议。
需要评估现有系统的性能瓶颈?
提供关键业务路径、真实峰值、服务器与数据库配置,以及现有压测或故障记录,月弦科技可据此梳理优化顺序、实施范围和可验证的验收条件。