回到本文的问题上来,本文中有两个事务执行同样的动作,分别为先执行select ... for update获取排他锁,其次判断若为空,则执行insert动作,否则执行update动作。伪代码描述如下:
start transaction
// 1、查询数据
data = select for update(tenantId, storeId, skuId);
if (data == null) {
// 插入数据
insert(tenantId, storeId, skuId);
} else {
// 更新数据
update(tenantId, storeId, skuId);
}
end transaction
复制代码
现在对这两个事务所执行的动作进行逐一分析,如下表所示:
时间点事务A事务B潜在动作1开始事务开始事务2执行select ... for update操作事务A申请到IX 事务A申请到X,Gap Lock3执行select ... for update操作事务B申请到IX,与事务A的IX不冲突。 事务B申请到Gap Lock,Gap Lock可共存。4执行insert操作事务A先申请插入意向锁IX,与事务B的Gap Lock冲突,等待事务B的Gap Lock释放。5执行insert操作事务B先申请插入意向锁IX,与事务A的Gap Lock冲突,等待事务A的Gap Lock释放。6死锁检测器检测到死锁详细分析:
•时间点1,事务A与事务B开始执行事务
•时间点2,事务A执行select ... for update操作,执行该操作时首先需要申请意向排他锁IX作用于表上,接着申请到了排他锁X作用于区间,因为查询的值不存在,故Next key Lock退化为Gap Lock。
•时间点3,事务B执行select ... for update操作,首先申请意向排他锁IX,根据2.1.3节表级锁兼容矩阵可以看到,意向锁之间是相互兼容的,故申请IX成功。由于查询值不存在,故可以申请X的Gap Lock,而Gap Lock之间是可以共存的,不论是共享还是排他。这一点可以参考Innodb关于Gap Lock的描述,关键描述本文粘贴至此:
select * from user
where mobile_num = 8 for update
复制代码
•时间点4,事务A执行insert操作前,首先会申请插入意向锁,但此时事务B已经拥有了插入区间的排他锁,根据2.1.3节表级锁兼容矩阵可知,在已有X锁情况下,再次申请IX锁是冲突的,需要等待事务B对X Gap Lock释放。
•时间点5,事务B执行insert操作前,也会首先申请插入意向锁,此时事务A也对插入区间拥有X Gap Lock,因此需要等待事务A对X锁进行释放。
•时间点6,事务A与事务B均在等待对方释放X锁,后被MySQL的死锁检测器检测到后,报Dead Lock错误。
思考:假如select ... for update 查询的数据存在时,会是什么样的过程呢?过程如下表:
时间点事务A事务B潜在动作1开始事务开始事务2执行select ... for update操作事务A申请到IX 事务A申请到X行锁,因数据存在故锁退化为Record Lock。3执行select ... for update操作事务B申请到IX,与事务A的IX不冲突。 事务B想申请目标行的Record Lock,此时需要等待事务A释放该锁资源。4执行update操作事务A先申请插入意向锁IX,此时事务B仅仅拥有IX锁资源,兼容,不冲突。然后事务A拥有X的Record Lock,故执行更新。5commit事务A提交,释放IX与X锁资源。6执行select ... for update操作事务B事务B此时获取到X Record Lock。7执行update操作事务B拥有X Record Lock执行更新8commit事务B释放IX与X锁资源也就是当查询数据存在时,不会出现死锁问题。
三、解决方法