线上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修改的中间,导致间隙锁被锁住而等待

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

相关推荐
Adolf_19931 小时前
Flask-JWT-Extended登录验证, 不用自定义
后端·python·flask
叫我:松哥1 小时前
基于Python flask的医院管理学院,医生能够增加/删除/修改/删除病人的数据信息,有可视化分析
javascript·后端·python·mysql·信息可视化·flask·bootstrap
海里真的有鱼1 小时前
Spring Boot 项目中整合 RabbitMQ,使用死信队列(Dead Letter Exchange, DLX)实现延迟队列功能
开发语言·后端·rabbitmq
工业甲酰苯胺2 小时前
Spring Boot 整合 MyBatis 的详细步骤(两种方式)
spring boot·后端·mybatis
新知图书2 小时前
Rust编程的作用域与所有权
开发语言·后端·rust
wn5313 小时前
【Go - 类型断言】
服务器·开发语言·后端·golang
希冀1233 小时前
【操作系统】1.2操作系统的发展与分类
后端
GoppViper4 小时前
golang学习笔记29——golang 中如何将 GitHub 最新提交的版本设置为 v1.0.0
笔记·git·后端·学习·golang·github·源代码管理
爱上语文5 小时前
Springboot的三层架构
java·开发语言·spring boot·后端·spring
serve the people5 小时前
springboot 单独新建一个文件实时写数据,当文件大于100M时按照日期时间做文件名进行归档
java·spring boot·后端