start transaction
// 1、查询数据
data = https://www.isolves.com/it/sjk/MYSQL/2023-09-27/select for update(tenantId, storeId, skuId);
if (data =https://www.isolves.com/it/sjk/MYSQL/2023-09-27/= 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 的描述,关键描述本文粘贴至此:
- 时间点 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 错误 。
时间点事务 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 执行更新8?commit事务 B 释放 IX 与 X 锁资源
也就是当查询数据存在时,不会出现死锁问题 。?
三、解决方法1、在事务开始之前,采用 CAS + 分布式锁来控制并发写请求 。分布式锁 key 可以设置为 store_skuId_version
2、事务过程可以改写为:
start transaction
// RR级别下,读视图
data = https://www.isolves.com/it/sjk/MYSQL/2023-09-27/select from table(tenantId, storeId, skuId)
if (data =https://www.isolves.com/it/sjk/MYSQL/2023-09-27/= null) {
// 可能出现写并发
insert
} else {
data = https://www.isolves.com/it/sjk/MYSQL/2023-09-27/select for update(tenantId, storeId, skuId)
update
}
end transaction
虽然解决了插入数据不存在时会出现的死锁问题,但是可能存在并发写的问题,第一个事务获得锁会首先插入成功,第二个事务等待第一个事务提交后,插入数据 , 因为数据存在了所以报错回滚 。
3、调整事务隔离级别为 RC , 在 RC 下没有 next key lock(注意,此处并不准确,RC 会有少部分情况加 Next key lock),故此时仅仅会有 record lock,所以事务 2 进行 select for update 时需要等待事务 1 提交 。
推荐阅读
- 在SpringBoot中通过Canal实现MySQL与Redis的数据同步
- MySQL如何与Redis保持数据一致性?
- PostgreSQL vs MySQL - 1000万数据批量插入,谁能略胜一筹
- 四大事务所是哪四大 会计四大事务所是哪四大
- MySQL数据导入的几种方法
- MySQL5.7 EOL后,国内免费数据库替代方案
- 用惨痛教训换来的156条MySQL设计规约
- 为什么越来越多的人选择PostgreSQL,放弃了MySQL
- MySQL 十几种索引类型,你都清楚吗?
- MySQL数据库备份与恢复策略:Java实践指南
