一次生产上的mysql死锁原因排查

最近在生产上遇到了一个mysql死锁的问题,花了一整天才排查出来,现在本地造出同种场景的死锁。

1、在CMD命令窗口敲入命令 

mysql -hlocalhost -uroot -p

2、进入数据库test

use test

3、建表语句:

CREATE TABLE `user` (
  `id` bigint(64) NOT NULL AUTO_INCREMENT,
  `username` varchar(128) COLLATE utf8mb4_bin DEFAULT '',
  `password` varchar(128) COLLATE utf8mb4_bin DEFAULT '',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `create_user` varchar(128) COLLATE utf8mb4_bin DEFAULT '',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin

4、表中造一条数据

INSERT INTO USER(username, PASSWORD)VALUES('li1', '123456');
 

5、设置数据库隔离级别

SELECT @@global.tx_isolation; 
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
 

6、设置锁等待时间

SET innodb_lock_wait_timeout=5000;
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
 

7、事务1的执行过程

SET AUTOCOMMIT=0;
START TRANSACTION;
INSERT IGNORE INTO USER(username, PASSWORD)VALUES('li1', '123456');
UPDATE USER SET PASSWORD='111111' WHERE username='li1';
COMMIT;

8、事务2的执行过程

SET AUTOCOMMIT=0;
START TRANSACTION;
INSERT IGNORE INTO USER(username, PASSWORD)VALUES('li1', '123456');
UPDATE USER SET PASSWORD='111111' WHERE username='li1';
COMMIT;

9、死锁原因分析

9.1 表中已存在记录li1,列username存在唯一主键

9.2 当事务1执行插入操作insert ignore时,检测到唯一主键冲突,这里会对索引li1加S锁,如果是不存在唯一主键冲突,则会加X锁;

INSERT IGNORE INTO USER(username, PASSWORD)VALUES('li1', '123456');

9.3 事务2执行插入操作insert ignore时,检测到唯一主键冲突,也会对索引li1加S锁;

INSERT IGNORE INTO USER(username, PASSWORD)VALUES('li1', '123456');

9.4 事务1执行修改操作,会对索引li1请求X锁

UPDATE USER SET PASSWORD='111111' WHERE username='li1';

9.5 事务2执行修改操作,会对索引li1请求X锁

UPDATE USER SET PASSWORD='111111' WHERE username='li1';

9.6 这样,事务1和事务2同时对索引li1添加了S锁,同时修改操作又同时请求X锁,都在等待对方释放S锁,这就导致了死锁。

这是一种表中存在记录的情况下,两个事务对唯一索引的同时并发操作导致的死锁

还有一种情况是在表中不存在记录的情况下,三个事务对唯一索引的同时并发操作导致的死锁,这个待分析。

 

关于插入锁,MYSQL的官方文档说明是:

INSERT sets an exclusive lock on the inserted row. This lock is an index-record lock, not a next-key lock (that is, there is no gap lock) and does not prevent other sessions from inserting into the gap before the inserted row.

Prior to inserting the row, a type of gap lock called an insert intention gap lock is set. This lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap. Suppose that there are index records with values of 4 and 7. Separate transactions that attempt to insert values of 5 and 6 each lock the gap between 4 and 7 with insert intention locks prior to obtaining the exclusive lock on the inserted row, but do not block each other because the rows are nonconflicting.

If a duplicate-key error occurs, a shared lock on the duplicate index record is set. This use of a shared lock can result in deadlock should there be multiple sessions trying to insert the same row if another session already has an exclusive lock.

参考博文:

https://blog.csdn.net/varyall/article/details/80219459

简单的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之间的插入意向锁,获取在插入行上的排它锁,但是不会被互相锁住,因为数据行并不冲突。

假设发生了一个唯一键冲突错误,那么将会在重复的索引记录上加读锁。当有多个session同时插入相同的行记录时,如果另外一个session已经获得该行的排它锁,那么将会导致死锁。

 


版权声明:本文为brushli原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。