在高并发情况下,不同事务持有资源而等待另一个资源锁时,而其他事务恰好相反,出现了循环等待,即出现了死锁。
一般的,最常见且最易理解的死锁形式是:
事务T1持有A的锁->等待B的锁
事务T2持有B的锁->等待A的锁
这样的形式即发生了死锁,后续会介绍死锁避免及死锁解决,现在来看看对同一个行操作,mysql是如何出现死锁了。
首先,T1进行
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT id FROM pet where id=10 for share;
+----+
| id |
+----+
| 10 |
+----+
1 row in set (0.00 sec)
T2进行
START TRANSACTION;
DELETE from pet where id=10;
##会提示
DELETE from pet where id=10;
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction
因为T1不是自动提交事务,查询的时候,hold持有共享锁,而此时T2开启事务并试图进行delete拿x锁,但处于一直等待中,当最后等待锁超时而自动放弃(innodb-lock-wait-timeout 默认是50s)。
做一下调整,如果T2还在等待T1释放锁时,T1进行同样的删除操作
T1:
mysql> DELETE from pet where id=10;
Query OK, 1 row affected (0.00 sec)
此时 T2:
DELETE from pet where id=10;
[Err] 1213 - Deadlock found when trying to get lock; try restarting transaction
过程:如果T1开启事务,先拿取了记录R的S读锁,此时T2来删除R需要用X锁,因T1占有S锁不得不等待,在此等待过程中,T1又进行了删除操作又尝试对R记录追加X锁,最后导致T2发生死锁退出。
死锁原因:
此处发生死锁,因为客户端T1需要X锁来删除该行。 但是,无法授予该锁请求,因为客户端T2已经有一个X锁请求并且正在等待客户端T1释放其S锁。 由于T2先前对X锁的请求,T1保持的S锁也不能升级到X锁。 因此,InnoDB会为其中一个客户端生成错误并释放锁。
启用死锁检测(默认设置)后,InnoDB会自动检测事务死锁并回滚事务或事务以打破死锁。 InnoDB尝试选择要回滚的小事务,其中事务的大小由插入,更新或删除的行数决定。
innodb-deadlock-detect[={OFF|ON}] 死锁检测,默认值为ON
如果innodb_table_locks = 1(默认值)和autocommit = 0,InnoDB知道表锁,并且它上面的MySQL层知道行级锁。否则,InnoDB无法检测到由MySQL LOCK TABLES语句设置的表锁或由InnoDB以外的存储引擎设置的锁的死锁。通过设置innodb_lock_wait_timeout系统变量的值来解决这些情况。
当InnoDB执行事务的完全回滚时,将释放由事务设置的所有锁。但是,如果由于错误而仅回滚单个SQL语句,则可能会保留由该语句设置的某些锁定。发生这种情况是因为InnoDB以一种格式存储行锁,以至于后来无法知道哪个锁由哪个语句设置。
如果SELECT在事务中调用存储的函数,并且函数中的语句失败,则该语句将回滚。此外,如果在此之后执行ROLLBACK,则整个事务回滚。
禁用死锁检测
在高并发系统上,当许多线程等待同一个锁时,死锁检测会导致速度减慢。有时,在发生死锁时,禁用死锁检测并依赖innodb_lock_wait_timeout设置进行事务回滚可能更有效。可以使用innodb_deadlock_detect配置选项禁用死锁检测。
使用以下技术处理死锁并降低其发生的可能性:
●在任何时候,发出SHOW ENGINE INNODB STATUS命令以确定最近死锁的原因。这可以帮助您调整应用程序以避免死锁。
●如果频繁出现死锁警告,请通过启用innodb_print_all_deadlocks配置选项来收集更多的调试信息。有关每个死锁的信息,而不仅仅是最新的死锁,都记录在MySQL错误日志中。完成调试后禁用此选项。
●如果由于死锁而失败,请始终准备重新发布事务。死锁并不危险。再试一次。
●保持交易持续时间短且不易发生,以减少交易。
●在进行一组相关更改后立即提交事务,以使它们不易发生冲突。特别是,不要使用未提交的事务使交互式mysql会话长时间保持打开状态。
●如果使用锁定读取(SELECT ... FOR UPDATE或SELECT ... FOR SHARE),请尝试使用较低的隔离级别,例如READ COMMITTED。
●在事务中修改多个表或同一个表中的不同行集时,每次都以一致的顺序执行这些操作。然后事务形成定义良好的队列,不会死锁。例如,将数据库操作组织到应用程序中的函数中,或调用存储的过程,而不是在不同的位置编写多个类似的INSERT,UPDATE和DELETE语句序列。
●在表中添加精心选择的索引。然后,您的查询需要扫描更少的索引记录,从而设置更少的锁。使用EXPLAIN SELECT确定MySQL服务器认为哪些索引最适合您的查询。
●使用较少的锁。如果您能够允许SELECT从旧快照返回数据,请不要向其添加FOR UPDATE或FOR SHARE子句。在这里使用READ COMMITTED隔离级别很好,因为同一事务中的每个一致读取都从其自己的新快照读取。
●如果没有其他帮助,请使用表级锁定序列化您的事务。将LOCK TABLES用于事务表(如InnoDB表)的正确方法是使用SET autocommit = 0(不是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表级锁定时,必须使用超时方法来解决死锁。
补:
你可以在my.ini配置文件中,使用innodb_read_io_threads和innodb_write_io_threads配置参数来配置为数据页读写I/O提供服务的后台线程的数量。这些参数表示用于读写请求的后台线程的数量。