MySQL 并发 insert on duplicate key 导致的死锁

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 一 前言死锁,其实是一个很有意思也很有挑战的技术问题,大概每个DBA和部分开发同学都会在工作过程中遇见 。关于死锁我会持续写一个系列的案例分析,希望能够对想了解死锁的朋友有所帮助。二 案例分析2.1 业务场景业务方的目的是使用insert on duplicate key update对重复存在的记...

一 前言

死锁,其实是一个很有意思也很有挑战的技术问题,大概每个DBA和部分开发同学都会在工作过程中遇见 。关于死锁我会持续写一个系列的案例分析,希望能够对想了解死锁的朋友有所帮助。

二 案例分析

2.1 业务场景

业务方的目的是使用insert on duplicate key update对重复存在的记录进行更新,没有则插入最新的记录。

另外需要特别注明的是我们最近对数据库进行升级,将数据库版本从Percona的5.6.24升级到5.7.22,业务在老版并没有死锁出现,但是升级到5.7.22版本的RR模式之后出现死锁

小插曲我们的数据库架构是 app-->rds proxy(lvs) --> DB模式的,一开始排查问题的时候以为DB实例设置RC模式,业务就是利用RC默认的事务隔离级别,但是万万没有想到rds proxy自己在连接初始化的时候设置了RR模式,导致之前基于RC模式的推断完全错误而且没有头绪。 说明我对整体技术框架的把控还不足。

2.2 环境说明

MySQL 5.7.22 事务隔离级别为RR模式。

CREATE TABLE `t` (
  id int(11) NOT NULL AUTO_INCREMENT,
  num int(10) unsigned DEFAULT NULL,
  val varchar(30) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  UNIQUE KEY `num_index` (`num`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;insert into t(num,val) values(10,'1'),(20,'2'),(30,'30'),
(50,'50'),(60,'60');

2.3 测试用例

sess1
begin;
insert into t(num,val) values(45,'45') on duplicate key update val='45'; 

sess2
begin;
insert into t(num,val) values(40,'40') on duplicate key update val='40';

sess1 
T3 insert into t(num,val) values(38,'38') on duplicate key update val='38';

T4 sess2 deadlock

2.4 死锁日志

*** (1) TRANSACTION:
TRANSACTION 24064444, ACTIVE 17 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 3603293, OS thread handle 139715684976384, query id 109761111 127.0.0.1 root update
insert into t(num,val) values(40,'40') on duplicate key update val='40'
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 219 page no 4 n bits 80 index num_index of table `test`.`t` trx id 24064444 lock_mode X locks gap before rec insert intention waiting
*** (2) TRANSACTION:
TRANSACTION 24064443, ACTIVE 31 sec inserting, thread declared inside InnoDB 5000
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 2
MySQL thread id 3603303, OS thread handle 139717801916160, query id 109761440 127.0.0.1 root update
insert into t(num,val) values(38,'38') on duplicate key update val='38'
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 219 page no 4 n bits 72 index num_index of table `test`.`t` trx id 24064443 lock_mode X locks gap before rec
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 219 page no 4 n bits 80 index num_index of table `test`.`t` trx id 24064443 lock_mode X locks gap before rec insert intention waiting
*** WE ROLL BACK TRANSACTION (1)

2.5 分析死锁日志

首先在分析死锁案例之前,我们先学习一下背景知识 insert 语句的加锁策略,来看看官方定义:

"INSERT sets an exclusive lock on the inserted row. This lock is an index-record lock, not a next-key lock (that is, there is no gap lock) and does not prevent other sessions from inserting into the gap before the inserted row.Prior to inserting the row, a type of gap lock called an insert intention gap lock is set. This lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap. "

相信大部分的DBA同行都知道在事务执行insert的时候会申请一把插入意向锁(Insert Intention Lock)。在多个会话并发写入不同数据记录至同一索引间隙的时候,并不需要等待其他事务完成,不会发生锁等待。

假设有一个索引记录包含键值4和7,不同的会话分别插入5和6,每个事务都会产生一个加在4-7之间的插入意向锁,获取在插入行上的排它锁,但是不会被互相锁住,因为数据行并不冲突。 但是如果遇到唯一键呢? 

"If a duplicate-key error occurs, a shared lock on the duplicate index record is set."

对于insert操作来说,若发生唯一约束冲突,则需要对冲突的唯一索引加上S Next-key Lock。从这里会发现,即使是RC事务隔离级别,也同样会存在Next-Key Lock锁,从而阻塞并发。然而,文档没有说明的是,对于检测到冲突的唯一索引,等待线程在获得S Lock之后,还需要对下一个记录进行加锁,在源码中由函数row_ins_scan_sec_index_for_duplicate进行判断。via (MySQL REPLACE死锁问题深入剖析 )。

划重点  上面的表述在针对5.6的时候没有问题。但是在5.7版本中就有疑问了,RR模式下insert on duplicate key update模式插入成功之后不仅仅是持有record lock 而且还持有一个Lock X GAP锁.

其次 我们需要了解锁的兼容性矩阵。

从兼容性矩阵我们可以得到如下结论:

INSERT操作之间不会有冲突。GAP,Next-Key会阻止Insert。GAP和Record,Next-Key不会冲突Record和Record、Next-Key之间相互冲突。已有的Insert锁不阻止任何准备加的锁。已经持有的gap 锁会阻塞插入意向锁INSERT_INTENTION 

另外 对于通过唯一索引更新或者删除不存在的记录,会申请加上 gap锁。

了解上面的基础知识,我们开始对死锁日志进行分析: 

T1 insert values(45,'45'),我们查看innodb_locks 

并没有显示锁相关内容,事情就这么简单吗?继续查询show engine innodb status 查看事务列表的信息:

---TRANSACTION 24064455, ACTIVE 63 sec
2 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
TABLE LOCK table `test`.`t` trx id 24064449 lock mode IX
RECORD LOCKS space id 219 page no 4 n bits 80 index num_index of table `test`.`t` trx id 24064449 lock_mode X locks gap before rec
--------

从结果来看T1时刻,sess1 持有表级别的意向锁LOCK IX 和2个X GAP LOCK,区间为 (30,45],(45,50]。

T2时刻 insert values(40,'40') ,我们查看 innodb_locks 信息

从事务列表来看T1 sess1 3 row lock(s) 持有表的意向锁,两个X locks gap before rec (30,45],(45,50] 以及 num=45的record lock(lock_mode X locks rec but not gap)

T2 sess2 insert values(40,'40') 持有表级别的锁意向锁IX ,2个locks gap before rec (30, 40],(40,45]的gap锁,准备插入的值40在(30,45)之间与sess1持有的X GAP lock冲突,发生等待locks gap before rec insert intention waiting 

T3 sess1 insert values(38,'38')和T2 sess2 类似,要插入38需要申请insert intention lock 与sess2持有的(30, 40) X GAP lock 冲突,于是等待sess2释放,至此我们可以理清楚死锁的顺序:

sess1(持有(30,45]的gap锁),sess2(持有(30, 40]的gap锁),sess1(T3 写入值38申请插入意向锁等待T2 sess2的gap锁释放) sess2(插入意向锁等待T1 sess1的gap锁释放)   构成循环等待,进而导致死锁。

2.6 解决方法

  1. 使用read commited 事务隔离级别。

  2. 降低事务中insert 次数。

  3. 使用队列,降低并发,比如单线程执行insert。

  4. 回退版本到5.6,显然对于我们而言不太现实。 5.6版本中 insert into t(num,val) values(45,'45') on duplicate key update val='45';对已经插入的记录num=45只会加上record lock,不会有额外的 gap lock。

三 小结

    本次死锁的核心因素是5.7版本之后对INSERT INTO .. ON DUPLICATE KEY 等语句的锁模式做了加强,除了行锁之外还有GAP lock。参见:

https://bugs.mysql.com/bug.php?id=38046
[3 Aug 2012 15:34] Paul Dubois
Noted in 5.7.0 changelog.
INSERT INTO ... SELECT ... ON DUPLICATE KEY UPDATE and LOAD DATA
CONCURRENT REPLACE took too weak a lock, leading to the possibility
of concurrent SELECT statements returning inconsistent results.

其他相关bug列表 

https://bugs.mysql.com/bug.php?id=38046 

https://bugs.mysql.com/bug.php?id=52020

四 推荐阅读

参考文章 https://zhuanlan.zhihu.com/p/29349080

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
19天前
|
SQL 关系型数据库 MySQL
遇到mysql数据库死锁,你会怎么排查?
遇到mysql数据库死锁,你会怎么排查?
53 0
|
24天前
|
存储 自然语言处理 关系型数据库
MySQL全文索引源码剖析之Insert语句执行过程
【8月更文挑战第17天】在MySQL中,处理含全文索引的`INSERT`语句涉及多步骤。首先进行语法解析确认语句结构无误;接着语义分析检查数据是否符合表结构及约束。随后存储引擎执行插入操作,若涉及全文索引则进行分词处理,并更新倒排索引结构。此外,事务管理确保了操作的完整性和一致性。通过示例创建含全文索引的表并插入数据,可见MySQL如何高效地处理此类操作,有助于优化数据库性能和提升全文搜索效果。
|
26天前
|
存储 SQL 关系型数据库
深入MySQL锁机制:原理、死锁解决及Java防范技巧
深入MySQL锁机制:原理、死锁解决及Java防范技巧
|
26天前
|
SQL JavaScript 关系型数据库
Mysql索引不当引发死锁问题
本文通过真实案例解析了MySQL在高并发环境下出现死锁的问题。数据库表`t_award`包含多个索引,但在执行特定SQL语句时遭遇索引失效,导致更新操作变慢并引发死锁。分析发现,联合索引`(pool_id, identifier, status, is_redeemed)`因`identifier`允许为空值而导致索引部分失效。此外,`pool_id`上的普通索引产生的间隙锁在高并发下加剧了死锁风险。为解决此问题,文中提出了调整索引顺序至`(pool_id, status, is_redeemed, identifier)`等方案来优化索引使用,进而减轻死锁现象。
|
1月前
|
Oracle 关系型数据库 MySQL
Mysql和Oracle数据库死锁查看以及解决
【8月更文挑战第11天】本文介绍了解决MySQL与Oracle数据库死锁的方法。MySQL可通过`SHOW ENGINE INNODB STATUS`查看死锁详情,并自动回滚一个事务解除死锁;也可手动KILL事务。Oracle则通过查询V$LOCK与V$SESSION视图定位死锁,并用`ALTER SYSTEM KILL SESSION`命令终止相关会话。预防措施包括遵循ACID原则、优化索引及拆分大型事务。
|
30天前
|
关系型数据库 MySQL
解决MySQL insert出现Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘xxx‘ at row 1
解决MySQL insert出现Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘xxx‘ at row 1
48 2
|
1月前
|
存储 关系型数据库 MySQL
在 MySQL 中使用 Insert Into Select
【8月更文挑战第11天】
52 0
在 MySQL 中使用 Insert Into Select
|
17天前
|
监控 关系型数据库 MySQL
MySQL死锁是什么
【8月更文挑战第26天】MySQL死锁是指两个或多个事务在执行过程中,因争夺锁资源而造成的相互等待的现象,若无外力干涉,它们都将无法继续执行。这种相互等待的情况会导致整个系统陷入停滞状态,影响数据库的性能和稳定性。
35 0
|
2月前
|
SQL 存储 关系型数据库
细说 MySQL 死锁
【7月更文挑战第26天】MySQL 死锁
28 4
|
2月前
|
SQL 存储 关系型数据库
细说 MySQL 死锁
死锁检查在MySQL 8.0中涉及三个主要步骤:构造锁等待图、初始化事务权重和提升权重。首先,当事务进入锁等待状态时,信息会被记录到内存中的`waiting_threads`,形成快照数组。接着,对这个数组进行排序,构造出锁等待图,表示事务间的等待关系。然后,初始化所有等待事务的权重为1,如果一个事务在其他事务等待后进入等待,其权重会被提升,以避免长时间等待。最后,根据锁等待图,提升那些同时阻塞其他事务的权重,但不包括参与死锁的事务。权重更新后,死锁检查线程将依据这些信息来检测和解决死锁。
64 15

热门文章

最新文章