MySQL 8.0 新特性之死锁检测控制

作者: 不剪发的Tony老师
毕业于北京航空航天大学,十多年数据库管理与开发经验,目前在一家全球性的金融公司从事数据库架构设计。CSDN学院签约讲师以及GitChat专栏作者。csdn上的博客收藏于以下地址:https://tonydong.blog.csdn.net


在 MySQL 中,如果两个不同的事务在执行时,互相持有了对方所需的锁,此时由于它们都在等待某个资源,永远不会释放自己获得的锁,因此就会产生死锁(deadlock)。

以下是一个产生死锁的示例。首先,在客户端 A 中创建一个表 t,它只有一行数据。然后开始一个事务,并且通过共享查询模式获取该行数据上的 S 锁。

mysql> CREATE TABLE t (i INT) ENGINE = InnoDB;
Query OK, 0 rows affected (1.07 sec)

mysql> INSERT INTO t (i) VALUES(1);
Query OK, 1 row affected (0.09 sec)

mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM t WHERE i = 1 FOR SHARE;
+------+
| i    |
+------+
|    1 |
+------+


接下来,在客户端 B 中开始另一个事务,并且尝试删除该行数据:

mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql> DELETE FROM t WHERE i = 1;


删除操作需要获得一个排他锁(X)。由于客户端 A 已经获得了一个 S 锁,客户端 B 的锁请求需要进入锁的等待队列,因此客户端 B 被阻塞。

最后,在客户端 A 中也尝试删除该行数据:

mysql> DELETE FROM t WHERE i = 1;
Query OK, 1 row affected (0.00 sec)



同时,在客户端 B 返回以下错误信息:

mysql> DELETE FROM t WHERE i = 1;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction



此时产生了死锁,因为客户端 A 需要获得一个 X 锁才能删除该行数据。然而,客户端 A 无法获得该锁,因为客户端 B 已经请求了一个 X 锁,并且等待客户端 A 释放该数据行上的 S 锁。客户端 A 上的 S 锁也无法升级为 X 锁,因为客户端 B 的 X 锁请求优先级更高。结果就是,InnoDB 将会在某个客户端产生错误,并且释放它所获取的锁。

至此,另一个客户端(A)能够获得请求的锁,并且删除该数据行。

通过以上示例可以看出,当启用了死锁检测时(默认设置),InnoDB 自动执行事务的死锁检测,并且回滚一个或多个事务以解决死锁。InnoDB 尝试回滚更小的事务,事务的大小由它所插入、更新或者删除的数据行数决定。

在 MySQL 8.0 中,增加了一个新的动态变量:innodb_deadlock_detect,可以用于控制 InnoDB 是否执行死锁检测。该参数的默认值为 ON,即打开死锁检测。

mysql> show variables like 'innodb_deadlock_detect';
+------------------------+-------+
| Variable_name          | Value |
+------------------------+-------+
| innodb_deadlock_detect | ON    |
+------------------------+-------+
1 row in set (0.00 sec)


对于高并发的系统,当大量线程等待同一个锁时,死锁检测可能会导致性能的下降。此时,如果禁用死锁检测,而改为依靠参数 innodb_lock_wait_timeout 执行发生死锁时的事务回滚可能会更加高效。

接下来,先在客户端 A 中关闭死锁检测:

mysql> set global innodb_deadlock_detect=off;
Query OK, 0 rows affected (0.04 sec)

 

重复上文中的示例,在客户端 A 中执行以下操作:

mysql> show variables like 'innodb_lock_wait_timeout';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout | 50    |
+--------------------------+-------+
1 row in set (0.00 sec)

mysql> INSERT INTO t (i) VALUES(1);
Query OK, 1 row affected (0.00 sec)

mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM t WHERE i = 1 FOR SHARE;
+------+
| i    |
+------+
|    1 |
+------+


在客户端 B 执行删除操作:

mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql> DELETE FROM t WHERE i = 1;



客户端 B 等待 X 锁,仍然被阻塞。再回到客户端 A,尝试删除该行数据:

mysql> DELETE FROM t WHERE i = 1;
Query OK, 1 row affected (37.87 sec)


此时客户端 B 不会提示死锁错误,而是等待 50 s (innodb_lock_wait_timeout 设置值)后提示等待超时:

mysql> DELETE FROM t WHERE i = 1;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction


同时客户端 A 在此之后成功删除数据,可以看到删除操作的时间为 37.87 s 。这个时间依取决于最后在客户端 A 中输入删除语句的时间。

通常来说,应该启用死锁检测,并且在应用程序中尽量避免产生死锁,同时对死锁进行相应的处理,例如重新开始事务。

只有在确认死锁检测影响了系统的性能,并且禁用死锁检测不会带来负面影响时,可以尝试关闭 innodb_deadlock_detect 选项。另外,如果禁用了 InnoDB 死锁检测,需要调整参数 innodb_lock_wait_timeout 的值,以满足实际的需求。

官方文档:MySQL 8.0 Reference Manual