• 首页
  • vue
  • TypeScript
  • JavaScript
  • scss
  • css3
  • html5
  • php
  • MySQL
  • redis
  • jQuery
  • InnoDB中的死锁

    死锁是指由于每个事务都持有对方需要的锁而无法进行其他事务的情况。因为这两个事务都在等待资源变得可用,所以两个都不会释放它持有的锁。

    当事务锁定多个表中的行(通过诸如UPDATE或的语句SELECT ... FOR UPDATE)但顺序相反时,可能会发生死锁。当此类语句锁定索引记录和间隙的范围时,由于时序问题,每个事务都获得了一些锁而没有获得其他锁,也会发生死锁。有关死锁的示例,请参见“ InnoDB死锁示例”。

    为了减少死锁的可能性,请使用事务而不是LOCK TABLES语句;保持插入或更新数据的事务足够小,以使其长时间不保持打开状态;当不同的事务更新多个表或大范围的行时,SELECT ... FOR UPDATE在每个事务中使用相同的操作顺序(例如);在SELECT ... FOR UPDATEUPDATE ... WHERE语句中使用的列上创建索引。死锁的可能性不受隔离级别的影响,因为隔离级别更改了读取操作的行为,而死锁则是由于写入操作而发生的。有关避免死锁状态并从死锁状态中恢复的更多信息,请参见“如何最小化和处理死锁”。

    启用死锁检测(默认设置)并且发生死锁后,将InnoDB检测条件并回滚其中一个事务(受害方)。如果使用innodb_deadlock_detect配置选项禁用了死锁检测,则在死锁的情况下InnoDB依靠该innodb_lock_wait_timeout设置回滚事务。因此,即使您的应用程序逻辑正确,您仍然必须处理必须重试事务的情况。要参见InnoDB用户事务中的最后一个死锁,请使用SHOW ENGINE INNODB STATUS命令。如果频繁出现死锁,说明事务结构或应用程序错误处理存在问题,请使用innodb_print_all_deadlocks启用此设置可将有关所有死锁的信息打印到mysqld错误日志中。有关如何自动检测和处理死锁的更多信息,请参见“死锁检测和回滚”。


    InnoDB死锁示例

    以下示例说明了锁定请求将导致死锁时如何发生错误。该示例涉及两个客户端A和B。

    首先,客户端A创建一个包含一行的表,然后开始事务。在事务中,A通过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锁。无法授予该S锁,因为它与客户端A持有的锁不兼容,因此该请求进入针对行和客户端B块的锁请求队列中。

    最后,客户端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锁定。由于B事先要求锁,因此SA持有的锁也不能升级XX锁。结果,InnoDB为其中一个客户端生成错误并释放其锁。客户端返回此错误:

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

    届时,可以授予对另一个客户端的锁定请求,并从表中删除该行。


    死锁检测和回滚

    当死锁检测被使能(缺省值),InnoDB自动检测事务的死锁和回退事务或交易打破僵局。InnoDB尝试选择要回滚的小事务,其中事务的大小由插入,更新或删除的行数确定。

    InnoDB知道表锁if innodb_table_locks = 1(默认)和autocommit = 0,它上面的MySQL层知道行级锁。否则,InnoDB无法检测死锁,该死锁是由MySQL LOCK TABLES语句设置的表锁或由存储引擎设置的锁InnoDB所涉及的锁。通过设置innodb_lock_wait_timeout系统变量的值来解决这些情况。

    InnoDB进行交易的完整回滚,由交易设置的所有锁都被释放。但是,如果由于错误而仅回滚单个SQL语句,则可以保留该语句设置的某些锁。发生这种情况是因为InnoDB以某种格式存储行锁,使得以后无法知道哪个语句设置了哪个锁。

    如果SELECT调用在事务中调用了存储的函数,而该函数内的一条语句失败,则该语句将回滚。此外,如果ROLLBACK在此之后执行,则整个事务都会回滚。

    如果LATEST DETECTED DEADLOCKInnoDB监视器输出包括一条消息指出,“过深或长时间的搜寻锁表WAITS-FOR图中,我们将回滚下面的事务,”这表明交易对所述等待名单已经达到了数限制为200。超过200个事务的等待列表将被视为死锁,并且尝试检查等待列表的事务将回滚。如果锁定线程必须参见等待列表上的事务所拥有的1,000,000个以上的锁,也可能发生相同的错误。

    有关组织数据库操作以避免死锁的技术,请参见“ InnoDB中的死锁”。

    禁用死锁检测

    在高并发系统上,当多个线程等待相同的锁时,死锁检测会导致速度变慢。有时,禁用死锁检测并在innodb_lock_wait_timeout发生死锁时依靠设置进行事务回滚可能会更有效。可以使用innodb_deadlock_detect配置选项禁用死锁检测。

    如何最小化和处理死锁

    本节以“死锁检测和回滚”中有关死锁的概念性信息为基础。它说明了如何组织数据库操作以最大程度地减少死锁和应用程序中所需的后续错误处理。

    死锁是事务数据库中的经典问题,但是除非死锁如此频繁以至于您根本无法运行某些事务,否则它们并不危险。通常,您必须编写应用程序,以便在由于死锁而使事务回滚时,它们始终准备重新发出事务。

    InnoDB使用自动行级锁定。即使在仅插入或删除单行的事务中,您也可能会陷入僵局。这是因为这些操作并不是真正的“原子”操作;它们会自动对插入或删除的行的(可能是多个)索引记录设置锁定。

    您可以使用以下技术来处理死锁并减少发生死锁的可能性:

    • 在任何时候,发出SHOW ENGINE INNODB STATUS命令以确定最近死锁的原因。这可以帮助您调整应用程序以避免死锁。
    • 如果频繁出现死锁警告引起关注,请通过启用innodb_print_all_deadlocks配置选项来收集更广泛的调试信息。有关每个死锁的信息,而不仅仅是最新的死锁,都记录在MySQL 错误日志中。完成调试后,请禁用此选项。
    • 如果由于死锁而失败,请始终准备重新发出事务。死锁并不危险。请再试一次。
    • 保持交易小巧且持续时间短,以使交易不易发生冲突。
    • 进行一系列相关更改后立即提交事务,以减少冲突的发生。特别是,不要长时间关闭未提交事务的交互式mysql会话。
    • 如果您使用锁定读取(SELECT ... FOR UPDATESELECT ... FOR SHARE),请尝试使用较低的隔离级别,例如READ COMMITTED
    • 修改事务中的多个表或同一表中的不同行集时,每次都要以一致的顺序执行这些操作。然后,事务形成定义良好的队列,并且不会死锁。例如,组织数据库操作到功能在应用程序中,或调用存储程序,而不是编码的多个相似序列INSERTUPDATE以及DELETE在不同的地方语句。
    • 将选择好的索引添加到表中。然后,您的查询需要扫描较少的索引记录,因此设置较少的锁。使用EXPLAIN SELECT以确定哪些索引MySQL认为最适合您的查询。
    • 使用更少的锁定。如果你能负担得起,以允许SELECT从一个旧的快照返回数据,不要添加条款FOR UPDATEFOR SHARE给它。在READ COMMITTED这里使用隔离级别是件好事,因为同一事务中的每个一致性读取均从其自己的新快照读取。
    • 如果没有其他帮助,请使用表级锁序列化事务。LOCK TABLES与事务表(例如InnoDB表)一起使用的正确方法是,以SET autocommit = 0(not START TRANSACTION)后跟来开始事务,直到明确提交事务后才LOCK TABLES调用UNLOCK TABLES。例如,如果您需要写表t1和从表中读取数据t2,则可以执行以下操作:

      SET autocommit=0;
      LOCK TABLES t1 WRITE, t2 READ, ...;... do something with tables t1 and t2 here ...
      COMMIT;
      UNLOCK TABLES;
      

      表级锁可防止对表的并发更新,从而避免死锁,但代价是对繁忙系统的响应速度较慢。

    • 序列化事务的另一种方法是创建一个仅包含一行的辅助“信号量”表。在访问其他表之前,让每个事务更新该行。这样,所有交易都以串行方式进行。请注意,InnoDB在这种情况下,即时死锁检测算法也适用,因为序列化锁是行级锁。对于MySQL表级锁,必须使用超时方法来解决死锁。