MySQL死锁排查

233 阅读5分钟

1. 死锁产生的原因

  MySQL 中发生死锁的原因通常是由于多个事务同时请求和持有了对数据库中的资源(如表、行、索引等)的排他性锁,并且彼此互相等待对方释放锁资源而无法继续执行的情况。以下是导致死锁的常见原因:

  • 并发事务:当多个事务同时并发地访问和修改数据库时,可能发生死锁。如果事务A持有资源X并等待资源Y,同时事务B持有资源Y并等待资源X,就会导致死锁。
  • 不同的锁顺序:如果多个事务以不同的顺序请求锁资源,可能导致死锁。例如,事务A先请求锁资源X,再请求锁资源Y,而事务B先请求锁资源Y,再请求锁资源X,这可能会导致死锁。
  • 长时间持有锁:如果一个事务长时间持有锁资源并阻塞其他事务的访问,可能导致其他事务等待的时间过长,从而增加了死锁的风险。
  • 不合理的索引和查询优化:如果数据库的索引设计不合理或查询语句没有进行正确的优化,可能会导致某些操作锁定了过多的资源,增加了死锁的可能性。
  • 数据库隔离级别:低隔离级别(如可重复读)下的事务并发访问数据库时,可能更容易发生死锁。隔离级别越高,发生死锁的可能性就越小。

2.两个示例和分析

  如果发生死锁的情况,这几个SQL能协助定位具体的死锁原因。

  • show engine innodb status : 通过运行该命令,您可以获取关于当前 InnoDB 引擎的各种信息,包括锁信息、事务信息、死锁信息等。;
  • select * from information_schema.innodb_locks : 通过查询该表,您可以获取有关当前锁定的对象、锁的类型、锁的模式等详细信息;
  • select * from information_schema.innodb_waits : 通过查询该表,您可以获取当前事务等待的锁资源的详细信息;
  • select * from information_schema.innodb_trx : 通过查询该表,您可以获取有关正在执行或挂起的事务的详细信息。

2.1 示例1

  有如下两个表:

CREATE TABLE `tblAccount` (

`uid` bigint(20) unsigned NOT NULL COMMENT 'id',

`money` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '金额',

`create_time` bigint(20) NOT NULL DEFAULT '0' COMMENT '创建时间',

`update_time` bigint(20) NOT NULL DEFAULT '0' COMMENT '更新时间',

PRIMARY KEY (`uid`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户账号';

CREATE TABLE `tblUserTask` (

`task_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',

`uid` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT 'uid',

`task_type` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '业务类型 ',

`task_ymw` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '年月周',

`money` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '金额',

`create_time` bigint(20) NOT NULL DEFAULT '0' COMMENT '创建时间',

`update_time` bigint(20) NOT NULL DEFAULT '0' COMMENT '更新时间',

PRIMARY KEY (`task_id`),

UNIQUE KEY `uniq_uid_tasktype_taskuniq` (`uid`,`task_type`)

) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8 COMMENT='用户任务';

  如果执行如下的操作,就会产生死锁。这个例子比较典型,加锁的顺序不一致导致,事务A先锁定tblUserTask中的资源,再锁定tblAccount的资源,事务B先锁定tblAccount中的资源,再锁定tblUserTask的资源。

事务A
步骤1
INSERT INTO `tblUserTask` (`uid`,`task_type`,`money`,`task_ymw`,`create_time`,`update_time`) VALUES (123456789,5,500,20230601,1685444032,1685444032);

步骤3
INSERT INTO `tblAccount` (`uid`,`money`,`create_time`,`update_time`) VALUES (123456789,500,1685444032,1685444032) ON DUPLICATE KEY UPDATE `money`=money + 5,`update_time`=1685444032;

事务B
步骤2
INSERT INTO `tblAccount` (`uid`,`money`,`create_time`,`update_time`) VALUES (123456789,500,1685444032,1685444032) ON DUPLICATE KEY UPDATE `money`=money + 2,`update_time`=1685444032;

步骤4
SELECT * FROM `tblUserTask` WHERE `uid` = 123456789 AND `task_ymw` = 20230601 FOR UPDATE;

  使用上面的排查语句来验证。在执行完事务A的步骤1,事务B的步骤2,事务A的步骤3以后。可以看到如下的信息:

image.png   执行事务B的步骤4以后,会看到死锁提示,事务B会自动回滚,事务A执行成功。show engine innodb status 可以看到如下的信息,通过这些日志,可以看到某个事务等待tblAccount表中的某个锁,另外一个事务拥有tblAccount表中的某个锁,等待tblUserTask表中的锁,造成死锁。

image.png

2.2 示例2

  如果执行如下的操作也会产生死锁,但是和上面的比起来,先的不够“典型”了。没有死锁经典的场景:两个事务加锁并且顺序不一致。看起来似乎是顺序一致,但是步骤3又确实等待步骤2的占用的锁,步骤4等待步骤1占用的锁。

事务A
步骤1
INSERT INTO `tblUserTask` (`uid`,`task_type`,`money`,`task_ymw`,`create_time`,`update_time`) VALUES (123456789,5,500,20230601,1685444032,1685289600);

步骤3
SELECT * FROM `tblUserTask` WHERE `uid` = 123456789 AND `task_ymw` = 20230601 FOR UPDATE;

事务B
步骤2
INSERT INTO `tblUserTask` (`uid`,`task_type`,`money`,`task_ymw`,`create_time`,`update_time`) VALUES (123456789,2,500,20230601,1685444032,1685289600);

步骤4
SELECT * FROM `tblUserTask` WHERE `uid` = 123456789 AND `task_ymw` = 20230601 FOR UPDATE;

3.常见的避免死锁方法

  • 合理设计数据库结构和索引,以减少锁冲突的概率。
  • 尽量缩小事务持有锁的范围和时间。
  • 按照相同的顺序请求锁资源,以避免不同的锁顺序导致死锁。
  • 使用合适的数据库隔离级别来平衡并发性和数据一致性的需求。