MySQL事务死锁问题排查 | 京东云技术团队

2023年 9月 26日 49.8k 0

一、背景

在预发环境中,由消息驱动最终触发执行事务来写库存,但是导致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引擎中有关于锁的内容

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为普通索引。

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

id user_id mobile_num
1 1 3
5 5 6
8 8 7
9 9 9

下面将使用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 = 6 and mobile_num

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论