17.7.5.3 死锁的最小化和处理
本节基于第17.7.5.2节,“死锁检测”中的概念信息,解释如何组织数据库操作以最小化死锁,并在应用程序中所需的错误处理。
死锁是事务型数据库中的经典问题,但它们只有在频繁到达不能运行某些事务时才会危险。通常,您必须编写应用程序,以便总是准备好重新提交事务,如果它因为死锁而被回滚。
InnoDB
使用自动行级锁定,即使只插入或删除单行记录,也可以出现死锁。这是因为这些操作不是真正的“原子”;它们自动设置了插入或删除行的索引记录上的锁定。
您可以使用以下技术来应对死锁和减少其发生的可能性:
-
在任何时候,执行
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
,直到你显式地提交事务。例如,如果你需要写入表t1
和从表t2
读取,可以这样做:SET autocommit=0; LOCK TABLES t1 WRITE, t2 READ, ...; ... do something with tables t1 and t2 here ... COMMIT; UNLOCK TABLES;
表级锁定防止了对表的并发更新,从而避免了死锁,但是在忙碌系统中可能会导致响应性下降。
-
另一种序列化事务的方法是创建一个辅助“信号量”表,该表只包含单行。让每个事务在访问其他表之前更新该行。在这种方式下,所有事务都将按照顺序执行。注意,
InnoDB
的实时死锁检测算法也可以在这个情况下工作,因为序列化锁是一个行级锁。使用 MySQL 表级锁定,需要使用超时方法来解决死锁。