|
1问题背景2MySQL锁回顾3DELETE流程4原因剖析5现场还原6问题思考6.1可以通过SELECTFORUPDATE避免吗6.2只有唯一索引会有这个问题吗6.3持有记录锁后再请求临键锁为什么需要等待6.4高版本的MySQL会存在DELETE死锁吗7事后总结8参考1问题背景“哥们,又双叒叕写了个死锁,秀啊!😏”就算是经常写死锁的同学看到估计都会有点懵,两条一模一样的DELETE语句怎么会产生死锁呢?2MySQL锁回顾看到这里的靓仔肯定对MySQL的锁非常了解,哥们还是带大家对锁的分类进行快速回顾;本文将基于MySQL5.7.21版本进行讨论,该版本使用InnoDB存储引擎,并采用RepeatedRead作为事务隔离级别。要查看MySQL的加锁信息,必须启用InnoDB状态监控功能;SET GLOBAL innodb_status_output=ON;SET GLOBAL innodb_status_output_locks=ON;要获取InnoDB存储引擎的详细状态信息,可以使用以下SQL命令;SHOW ENGINE INNODB STATUS; 3DELETE流程在深入分析问题原因之前先对DELETE操作的基本流程进行复习。众所周知,MySQL以页作为数据的基本存储单位,每个页内包含两个主要的链表:正常记录链表和垃圾链表。每条记录都有一个记录头,记录头中包括一个关键属性——deleted_flag。执行DELETE操作期间,系统首先将正常记录的记录头中的delete_flag标记设置为1。这一步骤也被称为deletemark,是数据删除流程的一部分。在事务成功提交之后,由purge线程负责对已标记为删除的数据执行逻辑删除操作。这一过程包括将记录从正常记录链表中移除,并将它们添加到垃圾链表中,以便后续的清理工作。针对不同状态下的记录,MySQL在加锁时采取不同的策略,特别是在处理唯一索引上记录的加锁情况。以下是具体的加锁规则:正常记录:对于未被标记为删除的记录,MySQL会施加记录锁,以确保事务的隔离性和数据的一致性。deletemark:当记录已被标记为删除(即delete_flag被设置为1),但尚未由purge线程清理时,MySQL会对这些记录施加临键锁,以避免在清理前发生数据冲突。已删除记录:对于已经被purge线程逻辑删除的记录,MySQL会施加间隙锁,这允许在已删除记录的索引位置插入新记录,同时保持索引的完整性和顺序性。4原因剖析在分析死锁的案例中,我们关注的表t_order_extra_item_15具有一个由(order_id,extra_key)组成的联合唯一索引。为了更好地理解死锁的产生机制,我们将对上述死锁日志进行简化处理。事务137060372(A)事务137060371(B)执行语句deletefromt_order_extra_item_15WHERE(order_id=xxxandextra_key=xxx)deletefromt_order_extra_item_15WHERE(order_id=xxxandextra_key=xxx)持有锁lock_modeXlocksrecbutnotgap(记录锁)等待锁lock_modeXlocksrecbutnotgapwaiting(记录锁)lock_modeXwaiting(临键锁)事务A试图获取记录锁,但被事务B持有的相同的记录锁所阻塞。而且,事务B在尝试获取临键锁时也遇到了阻塞,这是因为事务A先前已经请求了记录锁,从而形成了一种相互等待的状态,这种情况最终导致了死锁的发生。然而事务B为何在已经持有记录锁的情况下还需要等待临键锁?唯一合理的解释是,在事务B最初执行DELETE操作时,它所尝试操作的记录已经被其他事务锁定。当这个其他事务完成了deletemark并提交后,事务B不得不重新发起对临键锁的请求。经过深入分析得出结论,在并发环境中,必然存在另一个执行相同DELETE操作的事务,我们称之为事务C。通过仔细分析业务代码和服务日志,我们迅速验证了这一假设。现在,导致死锁的具体原因已经非常明显。为了帮助大家更好地理解三个事务的执行顺序,我们制定了一个事务执行时序的设想表格。事务A事务B事务C1.deletefromt_order_extra_item_15WHERE(order_id=xxxandextra_key=xxx))获取记录锁成功(lock_modeXlocksrecbutnotgap)2.deletefromt_order_extra_item_15WHERE(order_id=xxxandextra_key=xxx))等待获取记录锁(lock_modeXlocksrecbutnotgapwaiting)3.deletefromt_order_extra_item_15WHERE(order_id=xxxandextra_key=xxx))等待获取记录锁(lock_modeXlocksrecbutnotgapwaiting)4.deletemark设置记录头删除标识位delete_flag=15.事务提交6.获取记录锁成功记录状态变更重新获取临键锁(lock_modeX)7.发现死锁,回滚该事务WEROLLBACKTRANSACTION8.事务提交在执行流程的第6步中,事务B尝试重新获取临键锁,这时与事务A发生了相互等待的状况,导致死锁的发生。为解决这一问题,数据库管理系统自动回滚了事务A,以打破死锁状态。5现场还原哥们深知道理论分析至关重要,实践才是检验真理的唯一标准。Talkischeap,Showmethecode.在相同的系统环境下,我们创建了一个测试表来模拟实际情况;CREATE TABLE `t_lock` ( `id` int NOT NULL, `uniq` int NOT NULL, `idx` int NOT NULL,  RIMARY KEY (`id`), UNIQUE KEY `uniq` (`uniq`) USING BTREE, KEY `idx` (`idx`));INSERT INTO t_lock VALUES (1, 1, 1);INSERT INTO t_lock VALUES (5, 5, 5);INSERT INTO t_lock VALUES (10, 10, 10);大聪明一上来便直接手动开启3个MySQL命令列界面,每个界面中独立开启事务执行DELETEFROMt_lockwhereuniq=5;语句,然而实验结果并未能成功复现先前讨论的死锁状况。经过反复SHOWENGINEINNODBSTATUS;检查锁的状态得出结论:在DELETE操作中,加锁和deletemark是连续的不可分割的步骤,不受人为干预。一旦一个事务开始执行DELETE,其他事务对该记录的访问请求将自动转为临键锁,避免了死锁的发生。为了更准确地模拟并发环境下DELETE操作可能导致的死锁,这里采用Java语言编写了一个示例程序;public class Main { private static final String URL = "jdbc:mysql://localhost:3306/db_test"; private static final String USER = "root"; private static final String ASSWORD = "123456"; private static final String SQL = "DELETE FROM t_lock WHERE uniq = 5;"; public static void main(String[] args) { // 开启 3 个线程,模拟并发删除 for (int i = 0; i
|
|