本节基于 第 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
隔离级别很好,因为同一事务中的每个一致性读取都会从其自己的新快照中读取数据。如果其他方法都无法解决问题,请使用表级锁对事务进行序列化。在事务型表(例如
InnoDB
表)中使用LOCK TABLES
的正确方法是,使用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 表级锁的情况下,必须使用超时方法来解决死锁。