首页 > 应用开发 > IT综合 > MySQL 死锁日志分析方法
作者:京东物流 张凯
MySQL 死锁是线上经常遇到的现象,但是死锁分析却并不总是件容易的事情,本文介绍 MySQL 死锁日志的分析方法,帮助研发从日志中快速提取有效信息,从而提高死锁原因分析的效率。
死锁的触发条件包括四个:
如下图所示, 两个事务加锁顺序不同导致死锁。

发生死锁后只需要破坏发生死锁四个条件中的任意一个条件就可以解除死锁状态。数据库层面有两种策略用于打破死锁状态:
其中默认使用第二种策略,也就是检测到死锁后立即回滚,从而解除死锁状态。因此发生死锁时业务可能报错死锁,但不会报错锁等待超时。
innodb_deadlock_detect 参数用于控制是否开启死锁检测,该参数是 5.7.15 中引入。
mysql>select@@innodb_deadlock_detect;+--------------------------+|@@innodb_deadlock_detect|+--------------------------+| 1|+--------------------------+1rowinset(0.00 sec)
死锁检测本质上是一个搜索问题,5.7 中使用深度优先算法实现,具体是判断锁等待关系图中是否有环。
高并发场景下可以考虑关闭死锁检测,原因是如果锁等待队列很长,死锁检测成本高,会导致实例性能下降。但是前提是应用层面可以避免死锁,因此通常不建议关闭。
下面通过介绍一个死锁案例对死锁日志的格式与分析方法有一个感性认识。
------------------------LATEST DETECTED DEADLOCK------------------------2024-04-14 08:07:05 0x7fb6d39a6700*** (1) TRANSACTION:TRANSACTION 13020605130, ACTIVE 25 sec starting index readmysql tables in use 1, locked 1LOCK WAIT 33 lock struct(s), heap size 3520, 33 row lock(s), undo log entries 34MySQL thread id 2343498932, OS thread handle 140424015394560, query id 28769967039 x.x.x.x xwms_rw updatingUPDATE stock_occupy SET update_time = NOW() ,update_user = 'WAPS' ,qty_out_occupy=qty_out_occupy + 12.0000 WHERE map_area_id = 608 AND goods_no='EMG4418433215231' AND owner_no='0' AND lot_no='-1' AND product_level='100' AND org_no = '10' AND distribute_no = '10' AND warehouse_no = '126' AND map_area_id = 608*** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 127 page no 5255 n bits 272 index idx_map_goods_product_lot_owner of table `xwms`.`stock_occupy` trx id 13020605130 lock_mode X locks rec but not gap waiting Record lock, heap no 53 PHYSICAL RECORD: n_fields 6; compact format; info bits 0 0: len 8; hex 8000000000000260; asc `;; 1: len 16; hex 454d4734343138343333323135323331; asc EMG4418433215231;; 2: len 3; hex 313030; asc 100;; 3: len 2; hex 2d31; asc -1;; 4: len 1; hex 30; asc 0;; 5: len 8; hex 8000000000042de4; asc - ;;*** (2) TRANSACTION:TRANSACTION 13020606128, ACTIVE 10 sec starting index readmysql tables in use 1, locked 110 lock struct(s), heap size 1136, 7 row lock(s), undo log entries 8MySQL thread id 2343006037, OS thread handle 140423210886912, query id 28769967052 x.x.x.x xwms_rw updatingUPDATE stock_occupy SET update_time = NOW() ,update_user = 'WAPS' ,qty_out_occupy=qty_out_occupy + 11.0000 WHERE map_area_id = 608 AND goods_no='EMG4418442253742' AND owner_no='0' AND lot_no='-1' AND product_level='100' AND org_no = '10' AND distribute_no = '10' AND warehouse_no = '126' AND map_area_id = 608*** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 127 page no 5255 n bits 272 index idx_map_goods_product_lot_owner of table `xwms`.`stock_occupy` trx id 13020606128 lock_mode X locks rec but not gap Record lock, heap no 53 PHYSICAL RECORD: n_fields 6; compact format; info bits 0 0: len 8; hex 8000000000000260; asc `;; 1: len 16; hex 454d4734343138343333323135323331; asc EMG4418433215231;; 2: len 3; hex 313030; asc 100;; 3: len 2; hex 2d31; asc -1;; 4: len 1; hex 30; asc 0;; 5: len 8; hex 8000000000042de4; asc - ;;*** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 127 page no 5276 n bits 240 index idx_map_goods_product_lot_owner of table `xwms`.`stock_occupy` trx id 13020606128 lock_mode X locks rec but not gap waiting Record lock, heap no 38 PHYSICAL RECORD: n_fields 6; compact format; info bits 0 0: len 8; hex 8000000000000260; asc `;; 1: len 16; hex 454d4734343138343432323533373432; asc EMG4418442253742;; 2: len 3; hex 313030; asc 100;; 3: len 2; hex 2d31; asc -1;; 4: len 1; hex 30; asc 0;; 5: len 8; hex 8000000000044335; asc C5;;*** WE ROLL BACK TRANSACTION (2)
其中:
`id`bigint(20)NOTNULLAUTO_INCREMENTCOMMENT'自增id', `map_area_id`bigint(20)NOTNULLCOMMENT'地图区域ID', `goods_no`varchar(50)NOTNULLCOMMENT'商品编号', `product_level`varchar(50)NOTNULLCOMMENT'商品等级', `lot_no`varchar(50)NOTNULLCOMMENT'批次号', `owner_no`varchar(50)NOTNULLCOMMENT'货主编号', PRIMARYKEY(`id`), UNIQUEKEY`idx_map_goods_product_lot_owner`(`map_area_id`,`goods_no`,`product_level`,`lot_no`,`owner_no`)
其中:
下面介绍如何从死锁日志中获取有效信息,并分析其中最重要的信息-锁,包括锁的类型、不同类型锁的兼容性、常见加锁规则。
简化后的死锁日志格式如下所示。
InnoDB:***(1)TRANSACTION:InnoDB:***(1) WAITING FOR THIS LOCKTO BE GRANTED:InnoDB:***(2)TRANSACTION:InnoDB:***(2) HOLDS THE LOCK(S):InnoDB:***(2) WAITING FOR THIS LOCKTO BE GRANTED:InnoDB:*** WE ROLL BACK TRANSACTION(1)
其中主要信息包括:
其中存在的问题包括:
其中前两种信息的缺失直接导致死锁分析的难度增大,因此死锁原因分析通常需要反推来处理,也就是从等锁类型判断持锁类型。
缺少部分可以参考以下分析方法:
MySQL 中锁的粒度包括实例、表、行,其中后两种都可能导致死锁,本文假设都是行粒度,也就是行锁。
注意 行锁是给表的索引的记录加锁,且是给访问过的对象加锁。
死锁日志中与锁相关的信息包括:
当然也有其他因素影响加锁的类型,主要包括:
这些信息都可以认为是死锁案例的特征,其中 锁类型是最重要的特征。
锁类型(type_mode)主要包括以下三部分信息:
比如死锁日志中锁类型显示 lock_mode X locks rec but not gap waiting,其中:
注意锁的状态分两种,包括已获取到(GRANTED)与等待中(WAITING)。
死锁由两组锁等待组成,锁等待发生在锁冲突时,锁冲突根据锁兼容矩阵判断,下面介绍锁兼容矩阵。
不同类型行锁的兼容性见下表,其中第一行表示已有的锁,第一列表示要加的锁,❌ 表示锁冲突。
|
|
record | gap | next-key | insert intention |
| record | ❌ |
|
❌ |
|
| gap |
|
|
|
|
| next-key | ❌ |
|
❌ |
|
| insert intention |
|
❌ | ❌ |
|
其中:
加锁场景:
加锁类型:
加锁的单位是 next-key lock,部分场景下会发生退化,其中:
不退化的场景:
下面介绍一种高频锁冲突,那就是 插入时唯一键冲突加锁,注意加锁类型与事务隔离级别无关,这也是少见的 RC 中使用 gap lock 的场景。
当事务与未提交事务的唯一键冲突时:
insert 与 insert duplicate / replace 语句中唯一性检查时加锁模式不同:
常见的解决方案包括:
如下所示,自动分析死锁日志并将提取出来的特征显示在表格中,这里分析的是另一个死锁案例。

功能入口在【 易维-SRE开放平台-MySQL死锁分析】中,欢迎使用。

MySQL 死锁日志中的主要信息包括两个事务、两条 SQL、三部分锁信息。
其中锁信息又包括表、索引、锁类型、数据行。
此外,还包括实例级别的信息,包括数据库版本与事务隔离级别。
上述因素都会影响加锁规则,因此提供日志分析工具用于信息的自动提取。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70037994/viewspace-3044914/ ,如需转载,请注明出处,否则将追究法律责任。