线上Mysql死锁问题排查

一。背景:

我们线上有个漏洞扫描的项目,会添加一些主机扫描任务,但是有一次添加任务的时候,确失败了,对应的项目负责人排查了很久,也没有排查出来,于是请教我来排查这个问题,特意讲本次mysql死锁的问题排查思路记录下来

二。问题排查

2.1 查看mysql的死锁日志

命令:show engine innodb status;

查看死锁日志,这一块的死锁的数据很多,我已经把每行的注解添加了,数据如下:

java 复制代码
=====================================
2023-12-22 16:28:52 0xfffd53f391b0 INNODB MONITOR OUTPUT
#第二行是当前日期和时间
=====================================
Per second averages calculated from the last 23 seconds
是计算出这一平均值的时间间隔,即自上次输出以来的时间,或者是距上次内部复位的时长**
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 690554 srv_active, 0 srv_shutdown, 5607 srv_idle
# Srv_active:Master线程选择的active状态执行。Active数量增加与数据表、数据库更新操作有关,与查询无关,例如:插入数据、更新数据、修改表等;
# Srv_shutdown:这个参数的值一直为0,因为srv_shutdown只有在mysql服务关闭的时候才会增加;
# Srv_idle:这个参数是在master线程空闲的时候增加,即没有任何数据库改动操作时;



srv_master_thread log flush and writes: 696161
 


----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 419774776
OS WAIT ARRAY INFO: signal count 410126864

 **#这行给出了关于操作系统等待数组的信息,它是一个插槽数组,innodb在数组里为信号量保留了一些插槽,操作系统用这些信号量给线程发送信号,使线程可以继续运行,以完成它们等着做的事情,
 这一行还显示出innodb使用了多少次操作系统的等待:保留统计(reservation count)显示了innodb分配插槽的频度,而信号****计数(signal count)衡量的是线程通过数组得到信号的频度,操作系统的等待相对于空转等待(spin wait)要昂贵些。**


RW-shared spins 0, rounds 718913434, OS waits 353302131
#这行显示读写的共享锁的计数器

RW-excl spins 0, rounds 41119087, OS waits 828933
 #这行显示读写的排他锁的计数器

RW-sx spins 255378, rounds 7499854, OS waits 244550
Spin rounds per wait: 718913434.00 RW-shared, 41119087.00 RW-excl, 29.37 RW-sx

------------------------
LATEST DETECTED DEADLOCK
当服务器发生了死锁的情况时,这部分会显示出来。死锁通常的原因很复杂,但是这一部分只会显示最后两个发生死锁的事务,尽管可能也有其它事务也包含在死锁过程中。不过,尽管信息被删减了,通常你也能够通过这些信息找出死锁的原因。

2023-12-22 16:20:02 0xfffd52be31b0
*** (1) TRANSACTION:
TRANSACTION 1214688562, ACTIVE 0 sec fetching rows  # 这行表示事务 1214688562,ACTIVE 0 sec表示事务处于活跃状态0s,starting index read表示正在使用索引读取数据行
mysql tables in use 1, locked 1  #正在使用1个表,涉及锁的表有1个

LOCK WAIT 317 lock struct(s), heap size 41168, 10654 row lock(s)    
 #这行表示在等待317把锁,占用内存41168字节,涉及10654行记录,如果事务已经锁定了几行数据,
 这里将会有一行信息显示出锁定结构的数目(注意,这跟行锁是两回事)和堆大小,堆的大小指的是为了持有这些行锁而占用的内存大小,Innodb是用一种特殊的位图表来实现行锁的,从理论上讲,它可将每一个锁定的行表示为一个比特,经测试显示,每个锁通常不超过4比特**


MySQL thread id 171778, OS thread handle 281463492981168, query id 233344365 10.192.241.42 root updating
 #这行表示该事务的线程ID信息,操作系统句柄信息,连接来源、用户


 UPDATE av_task  SET agent_name='王惠金'  
 WHERE (agent_id LIKE '%fe9075c77850ea65c6df1f6ff65cda95531f000b7890d62d0c3e41e66421f7b7%' AND tenant_id = 2659458)

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 4505 page no 298 n bits 112 index PRIMARY of table `ahcloud_av`.`av_task` trx id 1214688562 lock_mode X waiting
#这行信息表示等待的锁是一个record lock,空间id是4505,页编号为298,大概位置在页的112位处,锁发生在表`ahcloud_av`.`av_task`的主键上,是一个X锁。 waiting表示正在等待锁


Record lock, heap no 39 PHYSICAL RECORD: n_fields 38; compact format; info bits 0
#这行表示record lock的heap no 位置

 0: len 30; hex 346665333932353963323234343837633935383264383433353037396266; asc 4fe39259c224487c9582d8435079bf; (total 32 bytes);
 1: len 6; hex 00004866ad28; asc   Hf (;;
 2: len 7; hex ad000029630139; asc    )c 9;;
 3: len 30; hex 36333865663130362d336664302d343030302d383030302d623130316537; asc 638ef106-3fd0-4000-8000-b101e7; (total 36 bytes);
 4: len 2; hex 6168; asc ah;;
 5: len 12; hex 303030343030303330303031; asc 000400030001;;
 6: len 8; hex 80009823b7abd002; asc    #    ;;
 7: len 12; hex e6af8fe591a8e4bbbbe58aa1; asc             ;;
 8: len 30; hex 626265386338303462333338376338346362316139373533646636343537; asc bbe8c804b3387c84cb1a9753df6457; (total 64 bytes);
 9: len 6; hex e4b881e5a68d; asc       ;;
 10: len 13; hex 35382e3231362e3138342e3138; asc 58.216.184.18;;
 11: len 17; hex 31382d36302d32342d38432d36352d3933; asc 18-60-24-8C-65-93;;
 12: len 4; hex 80000001; asc     ;;
 13: SQL NULL;
 14: len 4; hex 80000000; asc     ;;
 15: len 4; hex 80000001; asc     ;;
 16: len 5; hex 99b1f36500; asc    e ;;
 17: len 1; hex 80; asc  ;;
 18: len 1; hex 81; asc  ;;
 19: len 1; hex 81; asc  ;;
 20: len 0; hex ; asc ;;
 21: len 8; hex 80009823b7abd002; asc    #    ;;
 22: len 5; hex 99b1ed0502; asc      ;;
 23: SQL NULL;
 24: SQL NULL;
 25: len 30; hex 346665333932353963323234343837633935383264383433353037396266; asc 4fe39259c224487c9582d8435079bf; (total 32 bytes);
 26: len 4; hex 80000003; asc     ;;
 27: SQL NULL;
 28: len 4; hex 80000001; asc     ;;
 29: len 1; hex 80; asc  ;;
 30: len 13; hex 31373033353134303030303030; asc 1703514000000;;
 31: len 30; hex 353939353830303965616436343731376265623239363462326132626330; asc 59958009ead64717beb2964b2a2bc0; (total 32 bytes);
 32: len 1; hex 81; asc  ;;
 33: len 4; hex 80000000; asc     ;;
 34: len 8; hex 8000000000000000; asc         ;;
 35: len 8; hex 8000000000015180; asc       Q ;;
 36: SQL NULL;
 37: SQL NULL;

*** (2) TRANSACTION:
TRANSACTION 1214688552, ACTIVE 5 sec inserting   #事务2处于活跃状态5s
mysql tables in use 1, locked 1   #正在使用1个表,涉及锁的表有1个
4 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 4    #涉及4把锁,2行记录
MySQL thread id 172138, OS thread handle 281463480005040, query id 233344754 10.192.241.42 root update

INSERT INTO av_task  ( task_id,)  VALUES  ( '13d7e17e6fc64c399632a61e31357144') //ToDo 这里做了简化,只保留task_id字段

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 4505 page no 298 n bits 112 index PRIMARY of table `ahcloud_av`.`av_task` trx id 1214688552 lock_mode X locks rec but not gap
Record lock, heap no 39 PHYSICAL RECORD: n_fields 38; compact format; info bits 0
 0: len 30; hex 346665333932353963323234343837633935383264383433353037396266; asc 4fe39259c224487c9582d8435079bf; (total 32 bytes);
 1: len 6; hex 00004866ad28; asc   Hf (;;
 2: len 7; hex ad000029630139; asc    )c 9;;
 3: len 30; hex 36333865663130362d336664302d343030302d383030302d623130316537; asc 638ef106-3fd0-4000-8000-b101e7; (total 36 bytes);
 4: len 2; hex 6168; asc ah;;
 5: len 12; hex 303030343030303330303031; asc 000400030001;;
 6: len 8; hex 80009823b7abd002; asc    #    ;;
 7: len 12; hex e6af8fe591a8e4bbbbe58aa1; asc             ;;
 8: len 30; hex 626265386338303462333338376338346362316139373533646636343537; asc bbe8c804b3387c84cb1a9753df6457; (total 64 bytes);
 9: len 6; hex e4b881e5a68d; asc       ;;
 10: len 13; hex 35382e3231362e3138342e3138; asc 58.216.184.18;;
 11: len 17; hex 31382d36302d32342d38432d36352d3933; asc 18-60-24-8C-65-93;;
 12: len 4; hex 80000001; asc     ;;
 13: SQL NULL;
 14: len 4; hex 80000000; asc     ;;
 15: len 4; hex 80000001; asc     ;;
 16: len 5; hex 99b1f36500; asc    e ;;
 17: len 1; hex 80; asc  ;;
 18: len 1; hex 81; asc  ;;
 19: len 1; hex 81; asc  ;;
 20: len 0; hex ; asc ;;
 21: len 8; hex 80009823b7abd002; asc    #    ;;
 22: len 5; hex 99b1ed0502; asc      ;;
 23: SQL NULL;
 24: SQL NULL;
 25: len 30; hex 346665333932353963323234343837633935383264383433353037396266; asc 4fe39259c224487c9582d8435079bf; (total 32 bytes);
 26: len 4; hex 80000003; asc     ;;
 27: SQL NULL;
 28: len 4; hex 80000001; asc     ;;
 29: len 1; hex 80; asc  ;;
 30: len 13; hex 31373033353134303030303030; asc 1703514000000;;
 31: len 30; hex 353939353830303965616436343731376265623239363462326132626330; asc 59958009ead64717beb2964b2a2bc0; (total 32 bytes);
 32: len 1; hex 81; asc  ;;
 33: len 4; hex 80000000; asc     ;;
 34: len 8; hex 8000000000000000; asc         ;;
 35: len 8; hex 8000000000015180; asc       Q ;;
 36: SQL NULL;
 37: SQL NULL;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 4505 page no 99 n bits 112 index PRIMARY of table `ahcloud_av`.`av_task` trx id 1214688552 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 7 PHYSICAL RECORD: n_fields 38; compact format; info bits 0
 0: len 30; hex 313364383138373263383435343839376161383335393334303534656534; asc 13d81872c8454897aa835934054ee4; (total 32 bytes);
 1: len 6; hex 000043f38183; asc   C   ;;
 2: len 7; hex e70000026f1928; asc     o (;;
 3: len 30; hex 61383166613034622d346537392d343637612d383137332d653834376338; asc a81fa04b-4e79-467a-8173-e847c8; (total 36 bytes);
 4: len 2; hex 6168; asc ah;;
 5: len 12; hex 303030343030303630303031; asc 000400060001;;
 6: len 8; hex 80008b40596a9002; asc    @Yj  ;;
 7: len 13; hex e5ae9ae697b6e689abe68f8f33; asc             3;;
 8: len 30; hex 303639316635356236653836626430333562366363303832613839646362; asc 0691f55b6e86bd035b6cc082a89dcb; (total 64 bytes);
 9: len 30; hex e4b880e6a5bce8a5bfe58e85e887aae58aa9e4b880e58fb750432d323032; asc                         PC-202; (total 39 bytes);
 10: len 12; hex 36302e362e3231392e313533; asc 60.6.219.153;;
 11: len 17; hex 46342d34442d33302d46422d45462d3531; asc F4-4D-30-FB-EF-51;;
 12: len 4; hex 80000002; asc     ;;
 13: SQL NULL;
 14: len 4; hex 80000005; asc     ;;
 15: len 4; hex 80000001; asc     ;;
 16: len 5; hex 99b0dec849; asc     I;;
 17: len 1; hex 80; asc  ;;
 18: len 1; hex 81; asc  ;;
 19: len 1; hex 80; asc  ;;
 20: len 13; hex e5ae9ae697b6e689abe68f8f33; asc             3;;
 21: len 8; hex 80008b40596a9002; asc    @Yj  ;;
 22: len 5; hex 99b0dcc849; asc     I;;
 23: SQL NULL;
 24: SQL NULL;
 25: len 30; hex 343435303136393738323830346135633835396265343432616665303662; asc 4450169782804a5c859be442afe06b; (total 32 bytes);
 26: len 4; hex 80000003; asc     ;;
 27: SQL NULL;
 28: len 4; hex 80000000; asc     ;;
 29: len 1; hex 80; asc  ;;
 30: len 13; hex 31363931393837353839303337; asc 1691987589037;;
 31: len 30; hex 636239653434643831306266343839326163313765323665636666626638; asc cb9e44d810bf4892ac17e26ecffbf8; (total 32 bytes);
 32: len 1; hex 81; asc  ;;
 33: len 4; hex 80000000; asc     ;;
 34: len 8; hex 8000000000000000; asc         ;;
 35: len 8; hex 8000000000015180; asc       Q ;;
 36: SQL NULL;
 37: SQL NULL;

*** WE ROLL BACK TRANSACTION (2)
# 这个表示事务2被回滚,因为两个事务的回滚开销一样,所以选择了后提交的事务进行回滚,如果两个事务回滚的开销不同(undo 数量不同),那么就回滚开销最小的那个事务。
当一个事务持有了其他事务需要的锁,同时又想获得其他事务持有的锁时,等待关系图上就会产生循环,Innodb不会显示所有持有和等待的锁,
但是,它显示了足够的信息来帮你确定,查询操作正在使用哪些索引,这对于你确定能否避免死锁有极大的价值。
如果能使两个查询对同一个索引朝同一个方向进行扫描,就能降低死锁的数目,
因为,查询在同一个顺序上请求锁的时候不会创建循环,有时候,这是很容易做到的,
如:要在一个事务里更新许多条记录,就可以在应用程序的内存里把它们按照主键进行排序,然后,再用同样的顺序更新到数据库里,这样就不会有死锁发生,但是在另一些时候,这个方法也是行不通的(如果有两个进程使用了不同的索引区间操作同一张表的时候)。
------------
TRANSACTIONS
------------
Trx id counter 1214739434
#这行表示当前事务ID,这是一个系统变量,每创建一个新事务都会增加

Purge done for trx's n:o < 1214739432 undo n:o < 0 state: running but idle
#这是innodb清除旧MVCC行时所用的事务ID,将这个值和当前事务ID进行比较,就可以知道有多少老版本的数据未被清除。
这个数字多大才可以安全的取值没有硬性和速成的规定,如果数据没做过任何更新,那么一个巨大的数字也不意味着有未清除的数据,
因为实际上所有事务在数据库里查看的都是同一个版本的数据(此时只是事务ID在增加,而数据没有变更),
另一方面,如果有很多行被更新,那每一行就会有一个或多个版本留在内存里,减少此类开销的最好办法就是确保事务已完成就立即提交,
不要让它长时间地处于打开状态,因为一个打开的事务即使不做任何操作,也会影响到innodb清理旧版本的行数据。 
undo n:o < 0这个是innodb清理进程正在使用的撤销日志编号,为0 0时说明清理进程处于空闲状态。



History list length 250
 #历史记录的长度,即位于innodb数据文件的撤销空间里的页面的数目,如果事务执行了更新并提交,这个数字就会增加,而当清理进程移除旧版本数据时,它就会减少,清理进程也会更新Purge done for.....这行中的数值。

2.2 msyql死锁日志分析

从上面的日志中可以看到,是两个事物引起的死锁,这两个事物分别是

事务1:

UPDATE av_task SET agent_name='王惠金'

WHERE (agent_id LIKE '%fe9075c77850ea65c6df1f6ff65cda95531f000b7890d62d0c3e41e66421f7b7%' AND tenant_id = 2659458)
事务2

INSERT INTO av_task ( task_id,) VALUES ( '13d7e17e6fc64c399632a61e31357144') //ToDo 这里做了简化,只保留task_id字段

从代码中看,task_id是主键,但是这个主键没有使用自增的id,而是使用了uuid作为主键, 事务1是修改数据,事务2是插入数据,这样为什么会引起死锁呢?

2.2 继续分析死锁的原因:

按理说事务2在插入数据时候,会锁定这个数据,事务1由于在修改数据的时候,tenant_id和agent_id都没有索引,所以事务1会锁住所有的数据,就是事务1的锁被事务2的插入数据锁占用时候,也不会达到死锁的条件, 只需要等到事务2执行完之后,事务1就能继续执行了,所以需要进一步分析为什么会引起死锁呢?

2.3 从死锁的sql分析代码中做了什么流程

通过分析代码,事务2是在创建扫描任务的时候,插入的数据,这里把业务逻辑简单介绍下: 一个租户会有多个资产,当系统为这个租户创建扫描任务的时候,会先把这个租户的所有资产查出来,然后为每个资产添加一个扫描任务,代码可以模拟为:

事务2:

java 复制代码
for (AvTaskDTO taskDTO : avTaskDTOS) {
    insertAvTask(taskDTO);
}

事务1:

java 复制代码
 updateAvTask(taskDTO);

通过查询数据库数据,得知事务2在创建任务时候,会添加多个资产,这里为了简化,就假设添加两个资产的定时任务,而由于事务1在修改数据的时候,不会走索引,所以会一条一条锁数据,就会导致死锁,具体步骤我简化如下


假设表中有数据 A,D

1.事务2插入第一条数据成功,插入的数据假设为数据E

2.事务1开始修改数据,由于没有走索引,会一条一条锁住数据,AD锁定成功,当开始锁定E的时候,发现E事务2锁住了,于是事务1开始等待

3.事务2再次插入数据B,由于事务1在修改的时候,会锁住AD之间的间隙锁,所以导致事务2没法插入数据,等待事务1,就造成了死锁

总结为:

事务2持有锁(E),事务1持有锁(A,D)

事务1等待事务2E锁

事务2等待事务1D的Next-key锁

所以就导致了死锁,mysql在检测到死锁后,会释放其中一个事物,从而使其他事务得以继续执行, 数据的模拟如下:(线上数据拷贝到本地,执行这两个sql),如图所示;

如上图事务1在修改时候,会锁住这些数据

11f70590e39d4047b340d99504dad971

174c280a1ff24c388c2b24fcfc026281

而事务2第二次插入的数据主键是13d7e17e6fc64c399632a61e31357144

刚好位于二者之间,这个间隙锁被事务1锁住了,事务2只能等待

三,如何解决

msyql死锁原因分析到了,那么该怎么避免呢?

方式1: 事务1不要直接对表直接执行update操作,要先查出来数据,根据主键id在进行修改,这样就不会锁住所有的数据

方式2:不要使用uuid作为主键,因为uuid是随机的,可能会导致要插入的数据,刚好在其他sql修改的中间,导致间隙锁被锁住而等待

到这里死锁日志就排查出来了

相关推荐
加油,旭杏1 分钟前
【go语言】grpc 快速入门
开发语言·后端·golang
brzhang21 分钟前
墙裂推荐一个在 Apple Silicon 上创建和管理虚拟机的轻量级开源工具:lume
前端·后端
沈韶珺2 小时前
Visual Basic语言的云计算
开发语言·后端·golang
沈韶珺2 小时前
Perl语言的函数实现
开发语言·后端·golang
美味小鱼2 小时前
Rust 所有权特性详解
开发语言·后端·rust
我的K84092 小时前
Spring Boot基本项目结构
java·spring boot·后端
慕璃嫣3 小时前
Haskell语言的多线程编程
开发语言·后端·golang
晴空๓3 小时前
Spring Boot项目如何使用MyBatis实现分页查询
spring boot·后端·mybatis
Hello.Reader7 小时前
深入浅出 Rust 的强大 match 表达式
开发语言·后端·rust