如何减少和处理死锁 - MySQL 8.0官方文档笔记(四)


文档版本:8.0
来源:How to Minimize and Handle Deadlocks
上一篇:

本篇介绍如何减少死锁的发生,以及出现死锁时如何处理。
死锁指不同的事务因彼此持有对方等待的锁而不能继续执行的情形。因双方都在等待资源释放,任意一方都不会释放已有的锁。

正文

死锁是事务形数据库中的经典问题,但只要死锁的发生不会频繁到完全不能执行某个事务,那么就不算危险。通常,当事务因死锁而回滚时,你需要让你的应用随时做好重新发送事务的准备。

InnoDB引擎默认使用行锁。即使在事务中插入或删除单行数据,也能触发死锁。因为这些操作并不是真正的“原子的”,在插入或删除时它们会给行的索引值(可能有多个)上锁。

在前面的 中解释过:在默认级别REPEATABLE READ下,InnoDB在搜索和扫描索引时使用邻键锁,用于避免幻行。此时InnoDB会无视Where条件的过滤,给每个扫描到的索引值及其间隙上锁。此策略适用于加锁读、INSERT、DELETE,但有一个特殊场景只会上记录锁不会上间隙锁:WHERE条件中涵盖了唯一索引。

通过以下技巧,你可以处理好死锁,并减少其发生的概率:

  • 随时使用SHOW ENGINE INNODB STATUS找出最近发生死锁的原因,以便调整应用规避死锁。

  • 如果频繁的死锁警告惹人注目,开启innodb_print_all_deadlocks选项以收集额外的调试信息。在MySQL的错误日志中会记录每次死锁的信息,而不仅仅记录最后一次。完成调试后关闭这个选项。

  • 随时准备好重启因死锁而失败的事务。死锁并不危险,重试就好了。

  • 保持事务的短小精悍,以降低冲突的可能性。

  • 做出一系列关联变更后立即提交事务,以降低冲突的可能性。特别是不要让有关联的MySQL会话长时间挂起未提交的事务。

  • 如果使用加锁读(SELECT ... FOR UPDATESELECT ... FOR SHARE),尝试使用更低的隔离级别,如READ COMMITTED

  • 在同一事务内修改多张表,或一张表内的不同行时,每次以相同的顺序执行操作。以便让事务形成清晰的锁操作队列而规避死锁。例如,将数据库操作编排为应用内的函数,或调用SQL序列(即MySQL函数或存储过程),避免将类似的INSERT,UPDATE和DELETE操作分散在代码各处。

  • 精心设计表索引。让查询扫描更少的行数,也就意味着更少的锁。使用EXPLAIN SELECT查看MySQL认为查询最适合使用的索引。

  • 减少锁操作。如果能容忍查询返回老快照中的数据,就不要加FOR UPDATEFOR 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, ...;
    ... 在表t1和t2上操作 ...
    COMMIT;
    UNLOCK TABLES;
    

    表级锁防止表中的并发更改,通过牺牲响应度避免了死锁。

  • 另一种序列化事务的方式是创建辅助的“信号量”表,其中只包含一行数据。让每个事务在访问其它表之前先更新这一行。这样,所有事务将按串行方式执行。注意InnoDB的即时死锁检测算法同样适用于这种场景,因为此处序列化锁是一个行级锁。而对于MySQL的表级锁,必须使用超时方式来解决死锁。