orderPay 账户结算死锁分析与开发计划

2026-09-11 · store 项目 · 文档状态:方案待生产证据确认

结论:lock(true) 可能参与死锁,但单凭报错不能认定它是根因,也不应直接删除。应先取得完整死锁链,再统一锁顺序、强化幂等、缩短事务,并为 MySQL 1213 增加整事务有限重试。

事件与证据

POST /api/consume/orderPay 在写入 yoshop_account_settlement 时报告 MySQL 1213。该信息只说明数据库在这次 INSERT 期间检测到循环等待,无法单独确定另一个事务及其持有的锁。

本地代码确认入口位于 application/api/controller/Consume.php,同步/队列链路位于 application/api/service/Consume.php,账户结算位于 application/common/service/account/AccountSettlementService.php。

本文已移除认证密钥、支付码、消息标识及可识别字段;没有保存完整生产请求。

为什么不能直接删除 lock(true)

事务 A:锁 user → 写 account_settlement
事务 B:锁 account_settlement/相关索引 → 更新 user
结果:A 等 B,B 等 A,形成循环等待

SELECT ... FOR UPDATE 会延长账户记录排他锁的持有时间,是潜在参与因素。余额扣减需要并发保护,直接移除可能产生覆盖、重复扣款或超扣。真正要修复的是不同事务访问资源的顺序、重复消费和过长事务。

推荐方案

  1. 先取证:保存 SHOW ENGINE INNODB STATUS、相关表结构/索引、两个事务入口和消息投递时间线。
  2. 统一锁顺序:幂等判定 → 用户账户 → 按稳定顺序锁补贴记录 → 写流水 → 写结算 → 更新订单 → 提交。
  3. 业务幂等:业务订单号保证只结算一次,msgid 保证消息只消费一次;唯一约束上线前先查重。
  4. 整事务重试:1213 后完整回滚,随机退避,最多重试 2~3 次;每次重新读取状态。
  5. 缩短事务:把纯计算、非关键日志和外部调用移出持锁区。
  6. 核查索引:依据真实表结构、数据分布和 EXPLAIN 决定,不盲目增加索引。

Use Case 与完成条件

UC-STORE-SETTLEMENT-DEADLOCK-001:并发消费或重复消息到达时,系统只产生一次有效扣款和结算;1213 时整体回滚并有限重试;最终账户、流水、结算和订单一致。

DoR:BLOCKED。必须先补齐生产死锁报告、真实索引、相关写路径锁顺序和重复投递证据。

时间安排

阶段工期完成条件
取证与链路盘点0.5~1 天死锁双方 SQL、索引、入口和消息时间线齐备
方案评审0.5 天锁顺序、幂等键、重试、索引和回滚方案冻结
代码实现1~2 天统一锁顺序、有限重试、日志和幂等保护完成
并发与回归测试1~1.5 天并发、重复消息、消费/退款交叉及失败恢复通过
预发布和灰度观察1~2 天无重复扣款,对账一致,指标满足评审阈值

建议总投入:3.5~6 个工作日。历史数据清理、在线建索引或跨库一致性调整另行估算。

验收门禁

监控与应急

监控 1213 次数、重试成功率、结算失败率、接口 P95/P99、队列积压和重复消息。连续死锁时先限制同账户并发或暂停异常消费者扩容,保留现场并执行对账;不要用无限重试继续施压数据库。