B申请获取A持有的那个锁,但是被A占用着,所以B等待。

死锁的本质

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

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

为了减少死锁的可能性,请使用不是 LOCK TABLES 的事务语句;保持插入或更新数据的事务足够小,不使其长时间保持打开状态;当不同的事务更新多个表或大范围的行时,使用 SELECT ... FOR UPDATE 锁定所有要操作的行,并在每个事务中使用相同的操作顺序;在 SELECT ... FOR UPDATE UPDATE ... 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通过LOCK IN SHARE MODE来获得对行的读锁:

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开始事务并尝试从表中删除该行:

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事先要求锁,因此 S A持有的锁也不能 升级 X X 锁。结果, InnoDB 为其中一个客户端生成错误并释放其锁。客户端返回此错误。

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

在RR隔离级别下,数据库有两条数据id=1和id=10。 且id为普通索引,有两个事务, 事务1: select * from table where id=3 for update。 insert into table (id) values (3),他会在(1,3) (3,10)之间加间隙锁,在(3)这个位置申请插入意向锁。 事务2: select * from table where id=5 for update。insert into table (id) values (5)。 它会在(1,5) (5,10)之间加间隙锁,由于间隙锁互相兼容,故该锁可以获取,另外在(5)这个地方申请插入意向锁。 当事务1要获取插入意向锁时,发现(3)被事务2的间隙锁锁住了,故等待事务2释放锁; 事务2要获取插入意向锁时,发现(5)被事务1的间隙锁锁住了,故等待事务1释放锁;——死锁形成

银行转账的例子

create table money(id int primary key,price int);
insert into money values(1,1000);
insert into money values(2,1000);

事务A: 更新表,id=1的记录

mysql> start transaction;
Query OK, 0 rows affected (0.01 sec)
mysql> update money set price=2000 where id=1;
Query OK, 1 row affected (0.03 sec)

事务B: 更新表,id=2的记录

mysql> start transaction;
Query OK, 0 rows affected (0.01 sec)
mysql> update money set price=2000 where id=2;
Query OK, 1 row affected (0.00 sec)

事务A: 更新表,id=2的记录,此时会卡住(因为这条记录被加上了X锁)

mysql> update money set price=3000 where id=2;

事务B: 更新表,id=1的记录,此时会报错事务进行回滚,并且事务1会执行更新id=2的记录

mysql> update money set price=3000 where id=1;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

上述,事务抛出1213这个出错提示,即发生了死锁,上例中当两个事务都执行了第一条UPDATE语句,更新了一行数据,同时也锁定了该行数据,接着每个事务都尝试去执行第二条UPDATE语句,却发现该行已经被对方锁定,然后两个事务都等待对方释放锁,同时又持有对方需要的锁,则陷入死循环。除非有外部因素接入才可能解除死锁。为了解决这种问题,数据库系统实现了各种 死锁检测 和死锁超时 机制

死锁的必要条件

  • 多个并发事务(2个或者以上);
  • 每个事务都持有锁(或者是已经在等待锁);
  • 每个事务都需要再继续持有锁(为了完成事务逻辑,还必须更新更多的行);
  • 事务之间产生加锁的循环等待,形成死锁。
  • 总结:当两个或多个事务相互持有对方需要的锁时,就会产生死锁。

    死锁的检测

    当死锁检测启用时(默认),InnoDB会自动检测事务死锁并回滚一个或多个事务来打破死锁。InnoDB尝试选择小事务进行回滚,其中事务的大小由插入、更新或删除的行数决定。

    InnoDB在innodb_table_locks = 1(默认值)和autocommit = 0时知道表锁,它上面的MySQL层知道行锁。否则,InnoDB无法检测到由MySQL锁表语句设置的表锁,或由InnoDB以外的存储引擎设置的锁。通过设置innodb_lock_wait_timeout系统变量的值来解决这些情况。

    如果InnoDB监视器输出的最新检测到的死锁部分包含一条消息,“在锁表等待图中搜索太深或太长,我们将在事务之后回滚”,这表明等待列表中的事务数量已经达到了200的上限。 超过200个事务的等待列表被视为死锁,试图检查等待列表的事务被回滚。如果锁定线程必须查看等待列表中事务拥有的超过1,000,000个锁,也可能会发生同样的错误。

    死锁检测机制 - wait-for graph

    核心就是数据库会把事务单元锁维持的锁和它所等待的锁都记录下来,Innodb提供了 wait - for graph 算法来主动进行 死锁检测 ,每当加锁请求无法立即满足需要进入等待时, wait - for graph 算法都会被触发。当数据库检测到两个事务不同方向地给同一个资源加锁(产生循序),它就认为发生了死锁,触发 wait - for graph 算法。比如,事务1给A加锁,事务2给B加锁,同时事务1给B加锁(等待),事务2给A加锁就发生了死锁。那么死锁解决办法就是终止一边事务的执行即可,这种效率一般来说是最高的,也是主流数据库采用的办法。

    Innodb目前处理死锁的方法就是 将持有最少行级排他锁的事务进行回滚 。这也是相对比较简单的死锁回滚方式。死锁发生以后,只有部分或者完全回滚其中一个事务,才能打破死锁。对于事务型的系统,这是无法避免的,所以应用程序在设计必须考虑如何处理死锁。大多数情况下只需要重新执行因死锁回滚的事务即可。

    wait - for graph 原理

    我们怎么知道图中四辆车是死锁的?

    他们相互等待对方的资源,而且 形成环路 !我们将每辆车看为一个节点,当节点1需要等待节点2的资源时,就生成一条有向边指向节点2,最后形成一个有向图。我们只要检测这个有向图是否出现环路即可,出现环路就是死锁!这就是 wait - for graph 算法。

    Innodb将各个事务看为一个个节点,资源就是各个事务占用的锁,当事务1需要等待事务2的锁时,就生成一条有向边从1指向2,最后行成一个有向图。

    禁用死锁检测

    在高并发性系统中,当多个线程等待同一锁时,死锁检测可能导致速度下降。有时,禁用死锁检测并依赖于innodb_lock_wait_timeout设置在发生死锁时执行事务回滚可能更有效。可以使用innodb_deadlock_detect配置选项禁用死锁检测。

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

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

    你可以使用以下技巧来处理死锁,并降低发生死锁的可能性:

    在任何时候,发出显示引擎INNODB状态命令来确定最近死锁的原因。这可以帮助您优化应用程序以避免死锁。

    如果经常出现死锁警告,那么可以通过启用innodb_print_all_deadlocks配置选项来收集更多的调试信息。关于每个死锁的信息,而不仅仅是最近的死锁,都记录在MySQL错误日志中。完成调试后禁用此选项。

    如果事务由于死锁而失败,请随时准备重新发出事务。死锁并不危险。再试一次。

    保持事务较小且持续时间较短,以减少冲突的发生。

    在进行一组相关更改之后立即提交事务,以减少冲突的发生。特别是,不要让一个交互式mysql会话长时间打开一个未提交的事务。

    如果你使用锁读(SELECT ... FOR UPDATE 或 SELECT ... LOCK IN SHARE MODE),尝试使用较低的隔离级别,如READ COMMITTED。

    当修改一个事务中的多个表或同一表中的不同行集时,每次都要按照一致的顺序执行这些操作。这样,事务就形成了定义良好的队列,不会死锁。例如,将数据库操作组织成应用程序中的函数,或调用存储过程,而不是在不同的地方编写多个类似的INSERT、UPDATE和DELETE语句序列。

    向表中添加精心选择的索引。这样,查询需要扫描的索引记录就更少,因此设置的锁就更少。使用EXPLAIN SELECT来确定MySQL服务器认为哪些索引最适合您的查询。

    使用更少的锁定。如果允许SELECT从旧快照返回数据,则不要向其添加用于更新或锁定共享模式的子句。这里使用READ COMMITTED隔离级别很好,因为同一事务中的每次一致读取都是从它自己的新快照中读取的。

    如果没有其他帮助,使用表级锁序列化事务。对于事务性表,比如InnoDB表,使用锁表的正确方法是在事务开始时设置autocommit = 0(不是启动事务),然后是锁表,并且在显式提交事务之前不调用解锁表。例如,如果您需要写入表t1和读取表t2,您可以这样做:

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

    表级锁可以防止对表的并发更新,从而避免死锁,但对于繁忙的系统,响应能力会降低。

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

    优化表结构,优化schema,可在一定程度上避免死锁

    给记录集显式加锁时,最好一次性请求足够级别的锁。比如要修改数据的话,最好直接申请排他锁,而不是先申请共享锁,修改时再请求排他锁,这样容易产生死锁;

    尽量用相等条件访问数据,这样可以避免间隙锁对并发插入的影响; 不要申请超过实际需要的锁级别;除非必须,查询时不要显示加锁;