- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
在预发环境中,由消息驱动最终触发执行事务来写库存,但是导致MySQL发生死锁,写库存失败.
com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: rpc error: code = Aborted desc = Deadlock found when trying to get lock; try restarting transaction (errno 1213) (sqlstate 40001) (CallerID: ): Sql: "/* uag::omni_stock_rw;xx.xx.xx.xx:xxxxx;xx.xx.xx.xx:xxxxx;xx.xx.xx.xx:xxxxx;enable */ insert into stock_info(tenant_id, sku_id, store_id, available_num, actual_good_num, order_num, created, modified, SAVE_VERSION, stock_id) values (:vtg1, :vtg2, :_store_id0, :vtg4, :vtg5, :vtg6, now(), now(), :vtg7, :__seq0) /* vtgate:: keyspace_id:e267ed155be60efe */", BindVars: {__seq0: "type:INT64 value:"29332459" "_store_id0: "type:INT64 value:"50650235" "vtg1: "type:INT64 value:"71" "vtg2: "type:INT64 value:"113817631" "vtg3: "type:INT64 value:"50650235" "vtg4: "type:FLOAT64 value:"1000.000" "vtg5: "type:FLOAT64 value:"1000.000" "vtg6: "type:INT64 value:"0" "vtg7: "type:INT64 value:"20937611645" "}
初步排查,在同一时刻有两条请求进行写库存的操作.
时间前后相差1s,但最终执行结果是,这两个事务相互死锁,均失败.
事务定义非常简单,伪代码描述如下:
start transaction
// 1、查询数据
data = select for update(tenantId, storeId, skuId);
if (data == null) {
// 插入数据
insert(tenantId, storeId, skuId);
} else {
// 更新数据
update(tenantId, storeId, skuId);
}
end transaction
该数据库表的索引结构如下:
索引类型 | 索引组成列 |
---|---|
PRIMARY KEY | ( stock_id ) |
UNIQUE KEY | ( sku_id , store_id ) |
所使用的数据库引擎为Innodb,隔离级别为RR[Repeatable Read]可重复读.
首先了解下Innodb引擎中有关于锁的内容 。
在Innodb引擎中,行级锁的实现方式有以下三种:
名称 | 描述 |
---|---|
Record Lock | 锁定单行记录,在隔离级别RC和RR下均支持。 |
Gap Lock | 间隙锁,锁定索引记录间隙(不包含查询的记录),锁定区间为左开右开,仅在RR隔离级别下支持。 |
Next-Key Lock | 临键锁,锁定查询记录所在行,同时锁定前面的区间,故区间为左开右闭,仅在RR隔离级别下支持。 |
同时,在Innodb中实现了标准的行锁,按照锁定类型又可分为两类:
名称 | 符号 | 描述 |
---|---|---|
共享锁 | S | 允许事务读一行数据,阻止其他事务获得相同的数据集的排他锁。 |
排他锁 | X | 允许事务删除或更新一行数据,阻止其他事务获得相同数据集的共享锁和排他锁。 |
简言之,当某个事物获取了共享锁后,其他事物只能获取共享锁,若想获取排他锁,必须要等待共享锁释放;若某个事物获取了排他锁,则其余事物无论获取共享锁还是排他锁,都需要等待排他锁释放。如下表所示:
将获取的锁(下)\已获取的锁(右) | 共享锁S | 排他锁X |
---|---|---|
共享锁S | 兼容 | 不兼容 |
排他锁X | 不兼容 | 不兼容 |
假如现在有这样一张表user,下面将针对不同的查询请求逐一分析加锁情况。user表定义如下:
CREATE TABLE `user` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
`mobile_num` bigint(20) NOT NULL COMMENT '手机号',
PRIMARY KEY (`id`),
UNIQUE KEY `IDX_USER_ID` (`user_id`),
KEY `IDX_MOBILE_NUM` (`mobile_num`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户信息表'
其中主键id与user_id为唯一索引,user_name为普通索引.
假设该表中现有数据如下所示:
id | user_id | mobile_num |
---|---|---|
1 | 1 | 3 |
5 | 5 | 6 |
8 | 8 | 7 |
9 | 9 | 9 |
下面将使用select ... for update 语句进行查询,分别针对唯一索引、普通索引来进行举例.
select * from user
where id = 5 for update
select * from user
where user_id = 5 for update
在这两条SQL中,Innodb执行查询过程时,会如何加锁呢?
我们都知道Innodb默认的索引数据结构为B+树,B+树的叶子结点包含指向下一个叶子结点的指针。在查询过程中,会按照B+树的搜索方式来进行查找,其底层原理类似二分查找。故在加锁过程中会按照以下两条原则进行加锁:
1.只会对满足查询目标附近的区间加锁,并不是对搜索路径中的所有区间都加锁。本例中对搜索id=5或者user_id=5时,最终可以定位到满足该搜索条件的区域(1,5].
2.加锁时,会以Next key Lock为加锁单位。那按照1满足的区域进行加Next key Lock锁(左开右闭),同时因为id=5或者user_id=5存在,所以该Next key Lock会退化为Record Lock,故只对id=5或user_id=5这个索引行加锁.
如果查询的id不存在,例如:
select * from user
where id = 6 for update
按照上面两条原则,首先按照满足查询目标条件附近区域加锁,所以最终会找到的区间为(5,8]。因为id=6这条记录并不存在,所以Next key Lock(5, 8]最终会退化为Gap Lock,即对索引(5,8)加间隙锁.
select * from user
where id >= 4 and id <8 for update
同理,在范围查询中,会首先匹配左值id=4,此时会对区间(1,5]加Next key Lock,因为id=4不存在,所以锁退化为 Gap Lock(1,5);接着会往后继续查找id=8的记录,直到找到第一个不满足的区间,即Next key Lock(8, 9],因为8不在范围内,所以锁退化为Gap Lock(8, 9)。故该范围查询最终会锁的区域为(1, 9) 。
对非唯一索引查询时,与上述的加锁方式稍有区别。除了要对包含查询值区间内加Next key Lock之外,还要对不满足查询条件的下一个区间加Gap Lock,也就是需要加两把锁.
select * from user
where mobile_num = 6 for update
需要对索引(3, 6]加Next key Lock,因为此时是非唯一索引,那么也就有可能有多个6存在,所以此时不会退化为Record Lock;此外还要对不满足该查询条件的下一个区间加Gap Lock,也就是对索引(6,7)加锁。故总体来看,对索引加了(3,6]Next key Lock和(6, 7) Gap Lock.
若非唯一索引不命中时,如下:
select * from user
where mobile_num = 8 for update
那么需要对索引(7, 9]加Next key Lock,又因为8不存在,所以锁退化为Gap Lock (7, 9) 。
select * from user
where mobile_num >= 6 and mobile_num < 8
for update
首先先匹配mobile_num=6,此时会对索引(3, 6]加Next Key Lock,虽然此时非唯一索引存在,但是不会退化为Record Lock;其次再看后半部分的查询mobile_num=8,需要对索引(7, 9]加Next key Lock,又因为8不存在,所以退化为Gap Lock (7, 9)。最终,需要对索引行加Next key Lock(3, 6] 和 Gap Lock(7, 9).
Innodb为了支持多粒度锁定,引入了意向锁。意向锁是一种表级锁,用于表明事务将要对某张表某行数据操作而进行的锁定。同样,意向锁也分为类:共享意向锁(IS)和排他意向锁(IX).
名称 | 符号 | 描述 |
---|---|---|
共享意向锁 | IS | 表明事务将要对表的个别行设置共享锁 |
排他意向锁 | IX | 表明事务将要对表的个别行设置排他锁 |
例如select ... lock in shared mode会设置共享意向锁IS;select ... for update会设置排他意向锁IX 。
设置意向锁时需要按照以下两条原则进行设置:
1.当事务需要申请 行 的 共享锁S 时,必须先对 表 申请 共享意向IS锁 或更强的锁 。
2.当事务需要申请 行 的 排他锁X 时,必须先对 表 申请 排他意向IX锁 。
表级锁兼容性矩阵如下表:
将获取的锁(下)/已获取的锁(右) | X | IX | S | IS |
---|---|---|---|---|
X | 冲突 | 冲突 | 冲突 | 冲突 |
IX | 冲突 | 兼容 | 冲突 | 兼容 |
S | 冲突 | 冲突 | 兼容 | 兼容 |
IS | 冲突 | 兼容 | 兼容 | 兼容 |
如果请求锁的事务与现有锁兼容,则会将锁授予该事务,但如果与现有锁冲突,则不会授予该事务。事务等待,直到冲突的现有锁被释放.
意向锁的目的就是为了说明事务正在对表的一行进行锁定,或将要对表的一行进行锁定。在意向锁概念中,除了对全表加锁会导致意向锁阻塞外,其余情况意向锁均不会阻塞任何请求! 。
插入意向锁是一种特殊的意向锁,同时也是一种特殊的“Gap Lock”,是在Insert操作之前设置的Gap Lock.
如果此时有多个事务执行insert操作,恰好需要插入的位置都在同一个Gap Lock中,但是并不是在Gap Lock的同一个位置时,此时的插入意向锁彼此之间不会阻塞.
回到本文的问题上来,本文中有两个事务执行同样的动作,分别为先执行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 Lock | |
3 | 执行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的描述 ,关键描述本文粘贴至此:
Gap locks can co-exist. A gap lock taken by one transaction does not prevent another transaction from taking a gap lock on the same gap. There is no difference between shared and exclusive gap locks. They do not conflict with each other, and they perform the same function.
•时间点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,故执行更新。 | |
5 | commit | 事务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 = select from table(tenantId, storeId, skuId)
if (data == null) {
// 可能出现写并发
insert
} else {
data = 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提交.
参考文献 。
[1] Innodb锁官方文档: https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html 。
[2] https://blog.csdn.net/qq_43684538/article/details/131450395 。
[3] https://www.jianshu.com/p/027afd6345d5 。
[4] https://www.cnblogs.com/micrari/p/8029710.html 。
若有错误,还望批评指正 。
作者:京东零售 刘哲 。
来源:京东云开发者社区 转载请注明来源 。
最后此篇关于MySQL事务死锁问题排查的文章就讲到这里了,如果你想了解更多关于MySQL事务死锁问题排查的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
一、公平锁和非公平锁 1.1、公平锁和非公平锁的概述 公平锁:指多个线程按照申请锁的顺序来获取锁。 非公平锁:指在多线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取到锁
阅读目录 1、简介 2、分类 3、全局锁 4、表级锁 5、表锁 6、元数据锁
因此,在我编写的程序中,我有三个函数,为了简单起见,我们将它们称为 A、B 和 C。每个函数都需要访问资源X才能工作。 限制是A和B不允许同时运行并且必须适当同步。但是,C 可以与 A 或 B 同时运
我听说过这些与并发编程相关的词,但是锁、互斥量和信号量之间有什么区别? 最佳答案 锁只允许一个线程进入被锁定的部分,并且该锁不与任何其他进程共享。 互斥锁与锁相同,但它可以是系统范围的(由多个进程共享
这个问题已经有答案了: What is an efficient way to implement a singleton pattern in Java? [closed] (29 个回答) 已关闭
这个问题已经有答案了: What is an efficient way to implement a singleton pattern in Java? [closed] (29 个回答) 已关闭
我对标题中的主题有几个问题。首先,假设我们使用 JDBC,并且有 2 个事务 T1 和 T2。在 T1 中,我们在一个特定的行上执行 select 语句。然后我们对该行执行更新。在事务 T2 中,我们
我希望我的函数只运行一次。这意味着如果多个线程同时调用它,该函数将阻塞所有线程,只允许它运行。 最佳答案 听起来您希望存储过程进行同步。为什么不直接将同步放在应用程序本身中。 pthread_mute
if (runInDemoMode) { lock (this) { //Initalization of tables dCreator.create
我相信无论使用什么语言都可以考虑我的问题,但是为了有一些“ anchor ”,我将使用 Java 语言来描述它。 让我们考虑以下场景:我有一个扩展 Thread 的类 PickyHost 及其实例 p
我知道异步不是并行的,但我现在遇到了一个非常有趣的情况。 async function magic(){ /* some processing here */ await async () =
我们正在使用 Scala、Play 框架和 MongoDB(以 ReactiveMongo 作为我们的驱动程序)构建一个网络应用程序。应用程序架构是端到端的非阻塞。 在我们代码的某些部分,我们需要访问
我需要一个简单的锁,JavaME 超时(concurrent.lock 的反向移植需要完整的 Java 1.3)。 如果其他人已经为 JavaME 发布了经过测试的锁定代码,我宁愿使用它。 锁定是出了
根据 boost : To access the object, a weak_ptr can be converted to a shared_ptr using the shared_ptr co
关于 Mutex 和 Critical 部分之间的区别存在一个问题,但它也不处理 Locks。 所以我想知道临界区是否可以用于进程之间的线程同步。 还有信号状态和非信号状态的含义 最佳答案 在 Win
锁 最为常见的应用就是 高并发的情况下,库存的控制。本次只做简单的单机锁介绍。 直接看代码: 每请求一次库存-1. 假如库存1000,在1000个人请求之后,库存将变为0。
线程和进程 1、线程共享创建它的进程的地址空间,进程有自己的地址空间 2、线程可以访问进程所有的数据,线程可以相互访问 3、线程之间的数据是独立的 4、子进程复制线程的数据 5、子进程启动
**摘要:**细心的你也一定关注到,有的网址是https开头的,有的是http。https开头的网站前面,会有一把小锁。这是为什么呢? 本文分享自华为云社区《还不知道SSL证书已经是刚需了?赶快来了解
试图在 C 中实现一个非常简单的互斥锁(锁)我有点困惑。我知道互斥锁类似于二进制信号量,除了互斥锁还强制执行释放锁的线程的约束,必须是最近获得它的同一线程。我对如何跟踪所有权感到困惑? 这是我到目前为
在阅读了很多与上述主题相关的文章和答案之后,我仍然想知道 SQL Server 数据库引擎在以下示例中是如何工作的: 假设我们有一个名为 t3 的表: create table t3 (a int ,
我是一名优秀的程序员,十分优秀!