最近在工作中遇到一些死锁的问题,所以简单研究了一下后,写下一篇文章分享一下。
1.如何查看看mysql中出现的死锁?
通过show engine innodb status 查看的日志是最新一次记录死锁的日志。
通过查看死锁日志可以看到如下格式的日志
------------------------
LATEST DETECTED DEADLOCK
------------------------
2020-08-27 10:59:02 0x7f0a2a44d700
*** (1) TRANSACTION:
TRANSACTION 40124869845, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 23518445, OS thread handle 139681814148864, query id 159005367992 127.0.0.1 table updating
/*id:da7f25fb*/delete from table
where A = 1537169 and D = 10
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 3053 page no 264692 n bits 392 index idx_deal of table `database`.`table` trx id 40124869845 lock_mode X waiting
*** (2) TRANSACTION:
TRANSACTION 40124869844, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 1
MySQL thread id 23515886, OS thread handle 139681635555072, query id 159005367991 10.27.218.212 table update
/*id:a4f70f31*/insert into table (A, B, C, D)
VALUES (1537167, 150539097,2427520, 10)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 3053 page no 264692 n bits 392 index idx_deal of table `database`.`table` trx id 40124869844 lock mode S
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 3053 page no 264692 n bits 392 index idx_deal of table `database`.`table` trx id 40124869844 lock_mode X locks gap before rec insert intention waiting
*** WE ROLL BACK TRANSACTION (1)
------------
TRANSACTIONS
------------
2.如何读懂死锁日志
TRANSACTION 40124869845, ACTIVE 0 sec starting index read
事务编号为 40124869845 ,活跃0秒,starting index read 表示事务状态为根据索引读取数据。常见的其他状态:
fetching rows 表示事务状态在row_search_for_mysql中被设置,表示正在查找记录。
updating or deleting 表示事务已经真正进入了Update/delete的函数逻辑(row_update_for_mysql)
thread declared inside InnoDB 说明事务已经进入innodb层。通常而言 不在innodb层的事务大部分是会被回滚的。
(1)WAITING FOR THIS LOCK TO BE GRANTED: 表示事务(1)正在等待lock_mode X waiting 此处就是X锁的排它锁。
(2) HOLDS THE LOCK(S):表示事务40124869844 持有的lock mode s锁(S锁),同时(2) WAITING FOR THIS LOCK TO BE GRANTED:表示事务(2)正在等到lock_mode X locks gap before rec insert intention waiting锁也就是插入意向锁。
3.mysql死锁日志中出现的锁解释
3.1基本锁
首先我们要知道对于MySQL有两种常规锁模式
LOCK_S(读锁,共享锁)
LOCK_X(写锁,排它锁)
最容易理解的锁模式,读加共享锁,写加排它锁.
3.2 意向锁(Intention Locks)
InnoDB为了支持多粒度(表锁与行锁)的锁并存,引入意向锁。意向锁是表级锁,可分为意向共享锁(IS锁)和意向排他锁(IX锁)。
事务在请求S锁和X锁前,需要先获得对应的IS、IX锁。
意向锁产生的主要目的是为了处理行锁和表锁之间的冲突,用于表明“某个事务正在某一行上持有了锁,或者准备去持有锁”。
3.3 记录锁(也叫行锁)
记录锁(Record Locks)
记录锁, 仅仅锁住索引记录的一行。
单条索引记录上加锁,record lock锁住的永远是索引,而非记录本身,即使该表上没有任何索引,那么innodb会在后台创建一个隐藏的聚集主键索引,那么锁住的就是这个隐藏的聚集主键索引。所以说当一条sql没有走任何索引时,那么将会在每一条聚集索引后面加X锁,这个类似于表锁,但原理上和表锁应该是完全不同的。
3.4 间隙锁(Gap Lock)
区间锁, 仅仅锁住一个索引区间(开区间)。
在索引记录之间的间隙中加锁,或者是在某一条索引记录之前或者之后加锁,并不包括该索引记录本身。
3.5 next-key锁(Next-Key Locks)
record lock + gap lock, 左开右闭区间。
默认情况下,innodb使用next-key locks来锁定记录。
但当查询的索引含有唯一属性的时候,Next-Key Lock 会进行优化,将其降级为Record Lock,即仅锁住索引本身,不是范围。
3.6插入意向锁(Insert Intention Locks)
Gap Lock中存在一种插入意向锁(Insert Intention Lock),在insert操作时产生。在多事务同时写入不同数据至同一索引间隙的时候,并不需要等待其他事务完成,不会发生锁等待。
假设有一个记录索引包含键值4和7,不同的事务分别插入5和6,每个事务都会产生一个加在4-7之间的插入意向锁,获取在插入行上的排它锁,但是不会被互相锁住,因为数据行并不冲突。
4.insert加锁过程
简单的insert会在insert的行对应的索引记录上加一个排它锁,这是一个record lock,并没有gap,所以并不会阻塞其他session在gap间隙里插入记录。
不过在insert操作之前,还会加一种锁,官方文档称它为insertion intention gap lock,也就是意向的gap锁。这个意向gap锁的作用就是预示着当多事务并发插入相同的gap空隙时,只要插入的记录不是gap间隙中的相同位置,则无需等待其他session就可完成,这样就使得insert操作无须加真正的gap lock。
假设有一个记录索引包含键值4和7,不同的事务分别插入5和6,每个事务都会产生一个加在4-7之间的插入意向锁,获取在插入行上的排它锁,但是不会被互相锁住,因为数据行并不冲突。
假设发生了一个唯一键冲突错误,那么将会在重复的索引记录上加读锁(lock s)。当有多个session同时插入相同的行记录时,如果另外一个session已经获得该行的排它锁,那么将会导致死锁。
5.insert死锁场景分析
创建一张表
CREATE TABLE `aa` (
`id` int(10) unsigned NOT NULL COMMENT '主键',
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '姓名',
`age` int(11) NOT NULL DEFAULT '0' COMMENT '年龄',
`stage` int(11) NOT NULL DEFAULT '0' COMMENT '关卡数',
PRIMARY KEY (`id`),
UNIQUE KEY `udx_name` (`name`),
KEY `idx_stage` (`stage`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
如果T1未rollback,而是commit的话,T2和T3会报唯一键冲突:ERROR 1062 (23000): Duplicate entry ‘6’ for key ‘PRIMARY’。
事务T1 提交以后等到的死锁日志如下:
------------------------
LATEST DETECTED DEADLOCK
------------------------
2020-08-28 12:28:34 700000a3f000
*** (1) TRANSACTION:
TRANSACTION 36831, ACTIVE 17 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1184, 3 row lock(s), undo log entries 1
MySQL thread id 38, OS thread handle 0x700000b0b000, query id 953 localhost root update
insert into t values (4,5)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 25 page no 4 n bits 72 index `idx_b` of table `test`.`t` trx id 36831 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 5 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000016; asc ;;
1: len 4; hex 8000000b; asc ;;
*** (2) TRANSACTION:
TRANSACTION 36832, ACTIVE 13 sec inserting
mysql tables in use 1, locked 1
3 lock struct(s), heap size 360, 2 row lock(s)
MySQL thread id 39, OS thread handle 0x700000a3f000, query id 954 localhost root update
insert into t values (4,5)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 25 page no 4 n bits 72 index `idx_b` of table `test`.`t` trx id 36832 lock_mode X locks gap before rec
Record lock, heap no 5 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000016; asc ;;
1: len 4; hex 8000000b; asc ;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 25 page no 3 n bits 72 index `PRIMARY` of table `test`.`t` trx id 36832 lock mode S locks rec but not gap waiting
Record lock, heap no 5 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 4; hex 80000004; asc ;;
1: len 6; hex 000000008fdf; asc ;;
2: len 7; hex 8d000001d00110; asc ;;
3: len 4; hex 80000005; asc ;;
*** WE ROLL BACK TRANSACTION (2)
死锁成因分析:
事务T1成功插入记录,并获得索引id=6上的排他记录锁(LOCK_X | LOCK_REC_NOT_GAP)。
紧接着事务T2、T3也开始插入记录,请求排他插入意向锁(LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION);但由于发生重复唯一键冲突,前文提到过,如果插入出现重复键冲突时,各自请求的排他记录锁(LOCK_X | LOCK_REC_NOT_GAP)转成共享记录锁(LOCK_S | LOCK_REC_NOT_GAP)。此时T2和T3分别持有了不冲突的lock s 的共享记录锁。
T1回滚释放索引id=6上的排他记录锁(LOCK_X | LOCK_REC_NOT_GAP),T2和T3都要请求索引id=6上的排他记录锁(LOCK_X | LOCK_REC_NOT_GAP)。
由于X锁与S锁互斥,对于T2来讲,需要获取id=6 的排它锁,由于T3此时获取了id=6 的S锁,所以T2要等到T3把锁释放了,同样的道理T3要等T2把S锁释放了。T2和T3都等待对方释放S锁。于是,死锁便产生了。
如果此场景下,只有两个事务T1与T2或者T1与T3,则不会引发如上死锁情况产生。
最后:打一个小广告,后续的文章会在微信公众号“程序员之家QAQ”推送,欢迎大家搜索关注~~