Skip to the content.

..

InnoDB中的死锁

参考:15.7.5 Deadlocks in InnoDB

死锁是指多个事务由于互相持有对方需要的锁, 谁也无法继续往下执行的情况。因为都在等待资源, 谁都不会释放自己持有的锁。

比如通过 UPDATE 或者 SELECT ... FOR UPDATE 之类的语句, 锁定多张表或者多个行时, 如果以相反的顺序来执行, 就可能会发生死锁。如果SQL语句需要锁定索引范围、或者锁定间隙时, 由于时序问题, 每个事务都只获取到了一部分锁时, 也会发生死锁。

要减少死锁产生的可能性:

  1. 请使用事务,尽量不要使用LOCK TABLES 语句;
  2. 让执行 insertupdate 的事务足够小, 这样事务开启的时间不会太长;
  3. 不同的事务更新多张表或者大范围的行时, 让每个事务都保持相同的操作顺序;
  4. SELECT ... FOR UPDATEUPDATE ... WHERE 语句用到的列创建索引。

产生死锁的可能性不受隔离级别的影响, 因为隔离级别只是改变了读取操作的行为, 而死锁则是由于写操作发生的

启用(默认开启的)死锁检测(deadlock detection)时, InnoDB会自动检测到哪里产生了死锁, 并自动回滚其中的一个事务(称为受害方, victim)。 如果使用 innodb_deadlock_detect 选项禁用了自动死锁检测, 则 InnoDB 只能通过 innodb_lock_wait_timeout 指定的超时时间来回滚事务。 即使应用程序逻辑是完全正确的, 也需要处理事务重试等情况。 我们可以使用 SHOW ENGINE INNODB STATUS 命令查看最近发生死锁的事务。 如果频繁发生死锁问题, 需要进行事务结构调整, 或者需要进行错误处理时, 可以对 mysqld 的启动参数指定 innodb_print_all_deadlocks 选项, 以将死锁相关的全部信息打印到错误日志中。

InnoDB死锁示例

下面通过示例来演示导致死锁时会发生怎样的错误。 这个示例中涉及两个客户端: A和B。

首先, 客户端A创建了一张表, 并插入一条数据, 然后开启事务。 在事务中, 客户端A通过共享模式查询,来获得该行的 S 锁:

# 客户端A
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 LOCK IN SHARE MODE;
+------+
| i    |
+------+
| 1    |
+------+

接下来,客户端B开启事务, 并尝试从表中删除这一行:

# 客户端B
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql> DELETE FROM t WHERE i = 1;

删除操作需要获取X锁。 但由于X锁与A客户端持有的S锁不兼容, 无法立即得到授权, 需要加入该行的锁请求等待队列进行排队, 客户端B因此被阻塞。

然后,客户端A也尝试从表中删除该行:

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

可以看到这里发生了死锁, 因为客户端A需要先获取X锁才能删除该行。 但由于客户端B请求 X 锁, 正在等待客户端A释放S锁, 所以客户端A的X锁请求不能被授予。 而且是B客户端先请求的 X 锁, 导致A持有的S锁也不能升级为X锁。 结果就是 InnoDB 让其中一个客户端产生错误, 并释放其持有的锁。 客户端返回的错误信息类似这样:

ERROR 1213 (40001): Deadlock found when trying to get lock;
try restarting transaction

届时, 另一个客户端的锁请求会被授予, 接着执行, 从表中删除这一行数据。

Deadlock Detection

InnoDB 默认会开启死锁检测(deadlock detection), 能自动检测到事务产生的“死锁”, 并自动回滚其中的一个或多个事务以打破死锁状态。 InnoDB 会尝试选择回滚较小的事务, 至于事务的大小判断, 则取决于已经 inserted, updated, 和 deleted 的行数。

默认情况下, innodb_table_locks = 1, 如果 autocommit = 0, InnoDB会感知到表锁, 上层的MySQL也能感知行级锁。 否则, 如果涉及到 MySQL LOCK TABLES 语句设置的表锁, 或者由其他存储引擎设置的锁, 那么 InnoDB 无法自动检测到死锁。 只能通过系统变量 innodb_lock_wait_timeout 设置的超时时间来解决这类情况。

如果 InnoDB Monitor 输出的 LATEST DETECTED DEADLOCK 一节中包含了这样的信息: “TOO DEEP OR LONG SEARCH IN THE LOCK TABLE WAITS-FOR GRAPH, WE WILL ROLL BACK FOLLOWING TRANSACTION,” 就表明在等待列表中的事务数量达到了200个的限制。 超过200个事务的等待列表将被视为死锁, 尝试检查等待列表的事务将被回滚。 如果等待列表中的事务持有了超过 100万个以上的锁, 还有锁线程要来检查, 也可能会发生相同的错误。

禁用死锁检测

可以使用 innodb_deadlock_detect 选项来禁用死锁检测。

在高并发系统中, 多个线程等待同一个锁时, 死锁检测会导致响应速度变慢。 有时候, 依靠 innodb_lock_wait_timeout 指定的超时时间来进行事务回滚, 可能比自动死锁检测的效率更高。

如何降低死锁概率, 处理死锁错误

死锁是事务型关系数据库中的典型问题, 但死锁并不可怕, 除非是频繁发生死锁而导致无法执行某些事务。 通常, 由于死锁错误导致事务发生回滚时, 我们的应用程序需要重新执行这个事务【有些业务可以由人工触发】。

InnoDB 使用自动行级锁。 即使在插入或删除单行数据的事务中, 也可能会产生死锁。 因为这些操作并不是真正的原子操作;插入或删除行对应的(一到多个)索引记录时, 数据库会自动上锁。

下面介绍的技术手段可以用来处理死锁, 并降低产生死锁的可能性: