orderPay 账户结算死锁分析与开发计划
结论: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 会延长账户记录排他锁的持有时间,是潜在参与因素。余额扣减需要并发保护,直接移除可能产生覆盖、重复扣款或超扣。真正要修复的是不同事务访问资源的顺序、重复消费和过长事务。
推荐方案
- 先取证:保存
SHOW ENGINE INNODB STATUS、相关表结构/索引、两个事务入口和消息投递时间线。 - 统一锁顺序:幂等判定 → 用户账户 → 按稳定顺序锁补贴记录 → 写流水 → 写结算 → 更新订单 → 提交。
- 业务幂等:业务订单号保证只结算一次,
msgid保证消息只消费一次;唯一约束上线前先查重。 - 整事务重试:1213 后完整回滚,随机退避,最多重试 2~3 次;每次重新读取状态。
- 缩短事务:把纯计算、非关键日志和外部调用移出持锁区。
- 核查索引:依据真实表结构、数据分布和 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 个工作日。历史数据清理、在线建索引或跨库一致性调整另行估算。
验收门禁
- 同一账户 20~50 笔并发消费无超扣,成功金额与余额变化一致。
- 相同业务订单或消息并发提交,只产生一次有效扣款和结算。
- 消费与退款交叉并发无持续死锁,订单、流水、结算和余额一致。
- 注入 1213 后完整回滚并重试;超过上限明确失败,不无限重试。
- 正常消费的响应、错误码、打印和通知行为不回退。
监控与应急
监控 1213 次数、重试成功率、结算失败率、接口 P95/P99、队列积压和重复消息。连续死锁时先限制同账户并发或暂停异常消费者扩容,保留现场并执行对账;不要用无限重试继续施压数据库。