一。背景:
我们线上有个漏洞扫描的项目,会添加一些主机扫描任务,但是有一次添加任务的时候,确失败了,对应的项目负责人排查了很久,也没有排查出来,于是请教我来排查这个问题,特意讲本次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
开始修改数据,由于没有走索引,会一条一条锁住数据,A
和D
锁定成功,当开始锁定E
的时候,发现E
被事务2
锁住了,于是事务1
开始等待3.
事务2
再次插入数据B
,由于事务1
在修改的时候,会锁住A
和D
之间的间隙锁,所以导致事务2没法插入数据,等待事务1,就造成了死锁总结为:
事务2
持有锁(E)
,事务1
持有锁(A,D)
事务1
等待事务2
的E锁
事务2
等待事务1
的D的Next-key锁
所以就导致了死锁,mysql在检测到死锁后,会释放其中一个事物,从而使其他事务得以继续执行, 数据的模拟如下:(线上数据拷贝到本地,执行这两个sql),如图所示;
如上图事务1在修改时候,会锁住这些数据
11f70590e39d4047b340d99504dad971
174c280a1ff24c388c2b24fcfc026281
而事务2第二次插入的数据主键是13d7e17e6fc64c399632a61e31357144
刚好位于二者之间,这个间隙锁被事务1锁住了,事务2只能等待
三,如何解决
msyql死锁原因分析到了,那么该怎么避免呢?
方式1
: 事务1不要直接对表直接执行update操作,要先查出来数据,根据主键id在进行修改,这样就不会锁住所有的数据
方式2
:不要使用uuid作为主键,因为uuid是随机的,可能会导致要插入的数据,刚好在其他sql修改的中间,导致间隙锁被锁住而等待
到这里死锁日志就排查出来了