[开发笔记]避免数据库死锁之全局一致锁顺序

在电商平台开发中,往往涉及多种类型的订单结算以及并发结算的问题,例如这样的场景:

  • 平台涉及国内官方订单、国内商户订单以及海外官方订单等多种订单
  • 在各种订单完成时,又根据不同的订单类型,涉及冻结金额扣减、利润账户分润、经纪人分润、商户分润、预付回款、卡主回款等流程,期间将会涉及使用数据库事务,对多个账户钱包记录的行锁后变更
  • 但由于项目早期开发的不规范,并没有固定加锁的顺序,并且又将入账出账的流程设计为串行,并没有设计为出账同步入账异步的功能,因此整体处理流程又较依赖于行锁
  • 不同的完成订单流程中用户钱包的加锁顺序不一致,这里可能是冻结金额扣减->经纪人分润->利润账户分润,而另一处是冻结金额扣减、利润账户分润->经纪人分润;同时,经纪人也可以作为买家,在完成自己的某类订单时先进行冻结金额扣减

以上的几种条件组合,也就为后续业务的死锁埋下了种子。

当某次出现死锁时,调用如下的语句查询状态信息,就发现了曾经出现过某处是先锁住经纪人用户然后再锁住利润账户,而另一处则相反,因此并发这两种订单且涉及相关的用户时将会出现问题。

先查看状态信息

SHOW ENGINE INNODB STATUS;

通过查看状态输出的LATEST DETECTED DEADLOCK这段,可以分析得到对应的表、查询语句以及冲突的记录,为后续的业务修复提供分析:

通过日志的原文片段,可以了解到:发生死锁的记录来自表mall.user_economy,事务1已经得到了user_id = 14294(hex 000037d6)的钱包行锁,正在继续等待user_id = 6(hex 00000006)的行锁

2026-04-10 14:53:22 0x7fae0637b700
*** (1) TRANSACTION:
TRANSACTION 3322909277, ACTIVE 1 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 7 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 3
// ...ignore
SELECT version, balance, status FROM `user_economy` WHERE user_id = 6 FOR UPDATE
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 837 page no 41 n bits 216 index PRIMARY of table `mall`.`user_economy` trx id 3322909277 lock_mode X locks rec but not gap
Record lock, heap no 146 PHYSICAL RECORD: n_fields 29; compact format; info bits 128
 0: len 4; hex 000037d6; asc   7 ;;
 // ...ignore
 *** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 837 page no 6 n bits 152 index PRIMARY of table `mall`.`user_economy` trx id 3322909277 lock_mode X locks rec but not gap waiting
Record lock, heap no 5 PHYSICAL RECORD: n_fields 29; compact format; info bits 0
 0: len 4; hex 00000006; asc     ;;

继续查看事务2的日志,可以看到事务2已经拿到了user_id = 6的行锁,继续等待user_id = 14294的行锁

*** (2) TRANSACTION:
TRANSACTION 3322909275, ACTIVE 1 sec starting index read
mysql tables in use 1, locked 1
8 lock struct(s), heap size 1136, 5 row lock(s), undo log entries 5
// ...ignore
SELECT version, balance, status FROM `user_economy` WHERE user_id = 14294 FOR UPDATE
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 837 page no 6 n bits 152 index PRIMARY of table `mall`.`user_economy` trx id 3322909275 lock_mode X locks rec but not gap
Record lock, heap no 5 PHYSICAL RECORD: n_fields 29; compact format; info bits 0
 0: len 4; hex 00000006; asc     ;;

锁类型通过lock_mode X locks rec but not gap这里明确为行锁,而不是间隙锁

两个事务合起来就触发了因加锁顺序相反导致的经典的死锁:

事务 1:锁住 14294 -> 等待 6
事务 2:锁住 6     -> 等待 14294

而针对这种问题,或许会先想到统一分润的处理流程,即例如固定冻结金额扣减->经纪人分润->利润账户分润这样的顺序,多个订单类型都是固定处理的顺序是否就不会有问题了?

但是实际细想,处理顺序固定时遇到经纪人也可以作为买家也仍然会存在问题,同时遇到两类角色不同但用户相同的订单仍然会遇到死锁,因此其实可以通过用户ID降序或升序的形式来处理加锁

例如做这样的规范:永远先锁小 id,再锁大 id

保证事务2会直接等待事务1的第一个锁,而不会形成循环等待,这也是数据库并发里的经典原则:全局一致锁顺序(global lock ordering)

虽然并不意味着百分百无死锁,可能仍然存在:

  • gap lock
  • 索引选择不同
  • 隐式锁升级
  • 外键锁

但改造后至少能够处理80%以上的业务死锁。

发布者

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注