MySQL 事务死锁问题排查

一、背景 
在预发环境中,由消息驱动最终触发执行事务来写库存 , 但是导致 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 = 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
该数据库表的索引结构如下:
索引类型索引组成列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 + 树的搜索方式来进行查找,其底层原理类似二分查找 。故在加锁过程中会按照以下两条原则进行加锁:


推荐阅读