您当前的位置:首页 > 电脑百科 > 数据库 > MYSQL

MySQL 事务死锁问题排查

时间:2023-09-27 12:48:57  来源:京东云开发者  作者:

一、背景

 

在预发环境中,由消息驱动最终触发执行事务来写库存,但是导致 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" "}

初步排查,在同一时刻有两条请求进行写库存的操作。

MySQL 事务死锁问题排查

时间前后相差 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 引擎中有关于锁的内容

2.1 Innodb 中的锁

2.1.1 行级锁

在 Innodb 引擎中,行级锁的实现方式有以下三种:

名称描述Record Lock锁定单行记录,在隔离级别 RC 和 RR 下均支持。Gap Lock间隙锁,锁定索引记录间隙(不包含查询的记录),锁定区间为左开右开,仅在 RR 隔离级别下支持。Next-Key Lock临键锁,锁定查询记录所在行,同时锁定前面的区间,故区间为左开右闭,仅在 RR 隔离级别下支持。

同时,在 Innodb 中实现了标准的行锁,按照锁定类型又可分为两类:

名称符号描述共享锁S允许事务读一行数据,阻止其他事务获得相同的数据集的排他锁。排他锁X允许事务删除或更新一行数据,阻止其他事务获得相同数据集的共享锁和排他锁。

简言之,当某个事物获取了共享锁后,其他事物只能获取共享锁,若想获取排他锁,必须要等待共享锁释放;若某个事物获取了排他锁,则其余事物无论获取共享锁还是排他锁,都需要等待排他锁释放。如下表所示:

将获取的锁(下) 已获取的锁(右)共享锁 S排他锁 X共享锁 S兼容不兼容排他锁 X不兼容不兼容

2.1.2 RR 隔离级别下加锁示例

假如现在有这样一张表 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 为普通索引。

假设该表中现有数据如下所示:

iduser_idmobile_num113556887999

下面将使用 select ... for update 语句进行查询,分别针对唯一索引、普通索引来进行举例。

1、唯一索引等值查询

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) 加间隙锁。

2、唯一索引范围查询

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)

3、非唯一索引等值查询

对非唯一索引查询时,与上述的加锁方式稍有区别。除了要对包含查询值区间内加 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)

4、非唯一索引范围查询

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)。

2.1.3 意向锁(Intention Locks)

Innodb 为了支持多粒度锁定,引入了意向锁。意向锁是一种表级锁,用于表明事务将要对某张表某行数据操作而进行的锁定。同样,意向锁也分为类:共享意向锁(IS)和排他意向锁(IX)。

名称符号描述共享意向锁IS表明事务将要对表的个别行设置共享锁排他意向锁IX表明事务将要对表的个别行设置排他锁

例如 select ... lock in shared mode 会设置共享意向锁 IS;select ... for update 会设置排他意向锁 IX

设置意向锁时需要按照以下两条原则进行设置:

1. 当事务需要申请共享锁 S 时,必须先对申请共享意向 IS 锁或更强的锁

2. 当事务需要申请排他锁 X 时,必须先对申请排他意向 IX 锁

表级锁兼容性矩阵如下表:

将获取的锁(下)/ 已获取的锁(右)XIXSISX冲突冲突冲突冲突IX冲突兼容冲突兼容S冲突冲突兼容兼容IS冲突兼容兼容兼容

如果请求锁的事务与现有锁兼容,则会将锁授予该事务,但如果与现有锁冲突,则不会授予该事务。事务等待,直到冲突的现有锁被释放。

意向锁的目的就是为了说明事务正在对表的一行进行锁定,或将要对表的一行进行锁定。在意向锁概念中,除了对全表加锁会导致意向锁阻塞外,其余情况意向锁均不会阻塞任何请求!

2.1.4 插入意向锁

插入意向锁是一种特殊的意向锁,同时也是一种特殊的 “Gap Lock”,是在 Insert 操作之前设置的 Gap Lock。

如果此时有多个事务执行 insert 操作,恰好需要插入的位置都在同一个 Gap Lock 中,但是并不是在 Gap Lock 的同一个位置时,此时的插入意向锁彼此之间不会阻塞。

2.2 过程分析

回到本文的问题上来,本文中有两个事务执行同样的动作,分别为先执行 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 的描述,关键描述本文粘贴至此:

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,故执行更新。5commit事务 A 提交,释放 IX 与 X 锁资源。6执行 select ... for update 操作事务 B 事务 B 此时获取到 X Record Lock。7执行 update 操作事务 B 拥有 X Record Lock 执行更新8commit事务 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

若有错误,还望批评指正

 

作者:京东零售 刘哲
来源:京东云开发者社区 转载请注明来源


Tags:MySQL   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
MySQL 核心模块揭秘
server 层会创建一个 SAVEPOINT 对象,用于存放 savepoint 信息。binlog 会把 binlog offset 写入 server 层为它分配的一块 8 字节的内存里。 InnoDB 会维护自己的 savepoint...【详细内容】
2024-04-03  Search: MySQL  点击:(5)  评论:(0)  加入收藏
MySQL 核心模块揭秘,你看明白了吗?
为了提升分配 undo 段的效率,事务提交过程中,InnoDB 会缓存一些 undo 段。只要同时满足两个条件,insert undo 段或 update undo 段就能被缓存。1. 关于缓存 undo 段为了提升分...【详细内容】
2024-03-27  Search: MySQL  点击:(10)  评论:(0)  加入收藏
MySQL:BUG导致DDL语句无谓的索引重建
对于5.7.23之前的版本在评估类似DDL操作的时候需要谨慎,可能评估为瞬间操作,但是实际上线的时候跑了很久,这个就容易导致超过维护窗口,甚至更大的故障。一、问题模拟使用5.7.22...【详细内容】
2024-03-26  Search: MySQL  点击:(8)  评论:(0)  加入收藏
从 MySQL 到 ByteHouse,抖音精准推荐存储架构重构解读
ByteHouse是一款OLAP引擎,具备查询效率高的特点,在硬件需求上相对较低,且具有良好的水平扩展性,如果数据量进一步增长,可以通过增加服务器数量来提升处理能力。本文将从兴趣圈层...【详细内容】
2024-03-22  Search: MySQL  点击:(23)  评论:(0)  加入收藏
MySQL自增主键一定是连续的吗?
测试环境:MySQL版本:8.0数据库表:T (主键id,唯一索引c,普通字段d)如果你的业务设计依赖于自增主键的连续性,这个设计假设自增主键是连续的。但实际上,这样的假设是错的,因为自增主键不...【详细内容】
2024-03-10  Search: MySQL  点击:(5)  评论:(0)  加入收藏
准线上事故之MySQL优化器索引选错
1 背景最近组里来了许多新的小伙伴,大家在一起聊聊技术,有小兄弟提到了MySQL的优化器的内部策略,想起了之前在公司出现的一个线上问题,今天借着这个机会,在这里分享下过程和结论...【详细内容】
2024-03-07  Search: MySQL  点击:(26)  评论:(0)  加入收藏
MySQL数据恢复,你会吗?
今天分享一下binlog2sql,它是一款比较常用的数据恢复工具,可以通过它从MySQL binlog解析出你要的SQL,并根据不同选项,可以得到原始SQL、回滚SQL、去除主键的INSERT SQL等。主要...【详细内容】
2024-02-22  Search: MySQL  点击:(43)  评论:(0)  加入收藏
如何在MySQL中实现数据的版本管理和回滚操作?
实现数据的版本管理和回滚操作在MySQL中可以通过以下几种方式实现,包括使用事务、备份恢复、日志和版本控制工具等。下面将详细介绍这些方法。1.使用事务:MySQL支持事务操作,可...【详细内容】
2024-02-20  Search: MySQL  点击:(51)  评论:(0)  加入收藏
为什么高性能场景选用Postgres SQL 而不是 MySQL
一、 数据库简介 TLDR;1.1 MySQL MySQL声称自己是最流行的开源数据库,它属于最流行的RDBMS (Relational Database Management System,关系数据库管理系统)应用软件之一。LAMP...【详细内容】
2024-02-19  Search: MySQL  点击:(37)  评论:(0)  加入收藏
MySQL数据库如何生成分组排序的序号
经常进行数据分析的小伙伴经常会需要生成序号或进行数据分组排序并生成序号。在MySQL8.0中可以使用窗口函数来实现,可以参考历史文章有了这些函数,统计分析事半功倍进行了解。...【详细内容】
2024-01-30  Search: MySQL  点击:(53)  评论:(0)  加入收藏
▌简易百科推荐
MySQL 核心模块揭秘
server 层会创建一个 SAVEPOINT 对象,用于存放 savepoint 信息。binlog 会把 binlog offset 写入 server 层为它分配的一块 8 字节的内存里。 InnoDB 会维护自己的 savepoint...【详细内容】
2024-04-03  爱可生开源社区    Tags:MySQL   点击:(5)  评论:(0)  加入收藏
MySQL 核心模块揭秘,你看明白了吗?
为了提升分配 undo 段的效率,事务提交过程中,InnoDB 会缓存一些 undo 段。只要同时满足两个条件,insert undo 段或 update undo 段就能被缓存。1. 关于缓存 undo 段为了提升分...【详细内容】
2024-03-27  爱可生开源社区  微信公众号  Tags:MySQL   点击:(10)  评论:(0)  加入收藏
MySQL:BUG导致DDL语句无谓的索引重建
对于5.7.23之前的版本在评估类似DDL操作的时候需要谨慎,可能评估为瞬间操作,但是实际上线的时候跑了很久,这个就容易导致超过维护窗口,甚至更大的故障。一、问题模拟使用5.7.22...【详细内容】
2024-03-26  MySQL学习  微信公众号  Tags:MySQL   点击:(8)  评论:(0)  加入收藏
从 MySQL 到 ByteHouse,抖音精准推荐存储架构重构解读
ByteHouse是一款OLAP引擎,具备查询效率高的特点,在硬件需求上相对较低,且具有良好的水平扩展性,如果数据量进一步增长,可以通过增加服务器数量来提升处理能力。本文将从兴趣圈层...【详细内容】
2024-03-22  字节跳动技术团队    Tags:ByteHouse   点击:(23)  评论:(0)  加入收藏
MySQL自增主键一定是连续的吗?
测试环境:MySQL版本:8.0数据库表:T (主键id,唯一索引c,普通字段d)如果你的业务设计依赖于自增主键的连续性,这个设计假设自增主键是连续的。但实际上,这样的假设是错的,因为自增主键不...【详细内容】
2024-03-10    dbaplus社群  Tags:MySQL   点击:(5)  评论:(0)  加入收藏
准线上事故之MySQL优化器索引选错
1 背景最近组里来了许多新的小伙伴,大家在一起聊聊技术,有小兄弟提到了MySQL的优化器的内部策略,想起了之前在公司出现的一个线上问题,今天借着这个机会,在这里分享下过程和结论...【详细内容】
2024-03-07  转转技术  微信公众号  Tags:MySQL   点击:(26)  评论:(0)  加入收藏
MySQL数据恢复,你会吗?
今天分享一下binlog2sql,它是一款比较常用的数据恢复工具,可以通过它从MySQL binlog解析出你要的SQL,并根据不同选项,可以得到原始SQL、回滚SQL、去除主键的INSERT SQL等。主要...【详细内容】
2024-02-22  数据库干货铺  微信公众号  Tags:MySQL   点击:(43)  评论:(0)  加入收藏
如何在MySQL中实现数据的版本管理和回滚操作?
实现数据的版本管理和回滚操作在MySQL中可以通过以下几种方式实现,包括使用事务、备份恢复、日志和版本控制工具等。下面将详细介绍这些方法。1.使用事务:MySQL支持事务操作,可...【详细内容】
2024-02-20  编程技术汇    Tags:MySQL   点击:(51)  评论:(0)  加入收藏
MySQL数据库如何生成分组排序的序号
经常进行数据分析的小伙伴经常会需要生成序号或进行数据分组排序并生成序号。在MySQL8.0中可以使用窗口函数来实现,可以参考历史文章有了这些函数,统计分析事半功倍进行了解。...【详细内容】
2024-01-30  数据库干货铺  微信公众号  Tags:MySQL   点击:(53)  评论:(0)  加入收藏
mysql索引失效的场景
MySQL中索引失效是指数据库查询时无法有效利用索引,这可能导致查询性能显著下降。以下是一些常见的MySQL索引失效的场景:1.使用非前导列进行查询: 假设有一个复合索引 (A, B)。...【详细内容】
2024-01-15  小王爱编程  今日头条  Tags:mysql索引   点击:(82)  评论:(0)  加入收藏
站内最新
站内热门
站内头条