一次并发插入死锁带来的 MySql 锁知识点整理 |牛气冲天新年征文

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 最近遇到一个由于唯一性索引,导致并发插入产生死锁的场景,在分析死锁产生的原因时,发现这一块还挺有意思的,涉及到MySql中不少的知识点,特此总结记录一下

image.png


最近遇到一个由于唯一性索引,导致并发插入产生死锁的场景,在分析死锁产生的原因时,发现这一块还挺有意思的,涉及到MySql中不少的知识点,特此总结记录一下


I. MySql常见的锁



谈到mysql的锁,可以说的就比较多了,比如行锁、表锁、页锁、元数据锁等,当然我们这里没打算把所有的都细列出来,我们这里主要针对行锁、gap锁进行拓展,以方便分析第二节中,为什么并发插入同样的数据会产生死锁的问题


0. 锁分类


我们最常说的锁,可以区分为共享锁(S)和排它锁(X),在mysql的innodb引擎中,为了解决幻读问题,引入了gap锁以及next key lock;除此之外,还有一种意向锁的,比如插入意向锁


本文将主要介绍的以下几种锁


  • 行锁(record lock): 请注意它是针对索引的锁(所以如果没有索引时,最终行锁就会导致整个表都会被锁住)
  • 共享锁(S Lock): 也叫读锁,共享锁之间不会相互阻塞(顾名思义)
  • 排它锁(X Lock): 也叫写锁,排它锁一次只能有一个session(或者说事务?)持有
  • 间隙锁(gap lock): 针对索引之间的间隙
  • Next-key锁(Next-key lock):可以简单理解为行锁 + 间隙锁


上面虽然介绍了几种锁的基本定义,但是什么时候是行锁,怎样获取共享锁,排它锁又是在哪些场景下会产生呢?gap lock/next key lock又是怎样解决幻读的呢?


下面所有的都是基于mysql5.7.22 innodb引擎,rr隔离级别进行说明


1 共享锁与排它锁


下表介绍我们的实际使用的sql中,是否会使用锁,以及会产生什么锁


共享锁与排他锁区分

sql 示例 说明
select ... where select * from table limit 1 基于MVCC,快照读,不加锁
select ... for update select * from table where id=1 for update 排他锁
select ... lock in share mode select * from table where id=1 lock in share mode 共享锁
update ... where update table set xx=xx where id=1 排他锁
delete ... where delete table where id=1 排它锁


2. 行锁、表锁、gap锁、next-key锁区分


这几个的区分,主要就是看我们最终锁住的效果,如


  • 没有索引,加S/X锁最终都是锁整表 (为啥?因为锁是针对索引而言的)
  • 根据主键/唯一键锁定确定的记录:行锁
  • 普通索引或者范围查询:gap lock / next key lock

行锁和gap锁之间最大的区别是:

  • 行锁针对确定的记录
  • 间隙锁的是两个确定记录之间的范围; next key lock则是除了间隙还包括确定的记录


3. 实例演示


看上面的两个说明,自然就想在实际的case中操刀分析一下,不同的sql会产生什么样的锁效果


  • 针对表中一条确定的记录加X锁,是只有行锁嘛?
  • 针对表中多条确定的记录加X锁,又会怎样?
  • 针对表中一条不存在的记录加X锁,会有锁产生嘛?如果是gap锁,那区间怎么定?
  • 针对范围加X锁,产生的gap锁范围怎么确定呢?


在分析上面几种case之前,我们得先有一个概念,锁是针对索引而言的,这一点非常非常重要


其次不同的索引,我们需要分别进行测试(其实就是唯一索引与普通索引)


3.1 表准备


接下来针对上面的四种场景,设计我们的测试用例,首先我们准备三张表


  • 无索引表 TN
  • 唯一索引表 TU
  • 普通索引表 TI


对应的表结构和初始化数据如下


CREATE TABLE `tn` (
  `id` int(11) unsigned NOT NULL,
  `uid` int(11) unsigned NOT NULL
) ENGINE=InnoDB;
CREATE TABLE `tu` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `uid` int(11) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `u_uid` (`uid`)
) ENGINE=InnoDB;
CREATE TABLE `ti` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `uid` int(11) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  KEY `u_uid` (`uid`)
) ENGINE=InnoDB;
INSERT INTO `tn` (`id`, `uid`) VALUES (1, 10), (5, 20), (10, 30);
INSERT INTO `tu` (`id`, `uid`) VALUES (1, 10), (5, 20), (10, 30);
INSERT INTO `ti` (`id`, `uid`) VALUES (1, 10), (5, 20), (10, 30);
复制代码


3.2 精确匹配


即我们的sql可以精确命中某条记录时,锁情况如下


实例 TN TU TI
select * from tx where uid=20 for update 锁全表 行锁 uid=20 行锁uid=20, gap锁uid=[10, 30)


请注意上面的结论,无索引时锁全表好理解,但是普通索引的TI表,居然还有一个[10, 30)的gap锁就有点超乎我们的想象了;


接下来我们验证一下

image.png


上图基本流程如

image.png


从上面的实测也可以看出,普通索引下添加x锁,居然会加一个gap锁,而且这个gap区间是前一个记录(并包含它),到下一个记录


uid = 20, 前后两个记录为(1, 10), (10, 30)


  • gap lock: 范围为 [10, 30)
  • 因此无法插入uid=[10,30)
  • 注意,uid=10上有gap锁只是不能插入记录,但是加X锁是没有问题的(有兴趣的可以测试一下)


3.3 精确查询未匹配


当我们锁的记录不存在时,锁情况如下


实例 TN TU TI
select * from tx where uid=25 for update 锁全表 gap lock uid=(20,30) gap lock uid=(20, 30)


实测case如下(TN省略,锁全表的没啥测试必要性)


基本流程就不画图了,上面图中已经文字描述了

image.png


从上面的测试也可以看出,uid=30没有被锁住,这里只在uid=(20, 30)这一区间添加了gap锁


唯一索引与普通索引表现一致,会阻塞insert的插入意向锁(后面说这个东西)


3.4 范围查询


当我们锁一段区间时,锁情况如下


实例 TN TU TI
select * from tx where uid>15 and uid<25 for update 锁全表 next key lock uid=(10,30] next key lock uid=(10, 30]

image.png


简单来说,范围查询时,添加next key lock,根据我们的查询条件,找到最左边和最右边的记录区间


uid > 15 and uid < 25,找到的记录是 (1, 10), (10, 30)

  • gap锁为(10, 30)
  • next key lock会为右边添加行锁,即uid=30加X锁
  • 因此针对uid=30记录加锁会被阻塞(但是针对uid=28,29加x锁则不会被阻塞,插入会,有兴趣的小伙伴可以实测一下)


说明:范围加x锁时,可能锁住不再这个区间的记录,一不小心可能导致死锁哦


3.5 小结


在RR隔离级别中,我们一般认为可以产生锁的语句为:


  • SELECT ... FOR UPDATE: X锁
  • SELECT ... LOCK IN SHARE MODE: S锁
  • update/delete: X锁
索引 场景 锁范围
无索引 S/X锁 锁全表
唯一索引 精确匹配,且命中 行锁
唯一索引 精确匹配,未命中 gap lock
唯一索引 范围查询 next key lock (上个记录下个记录的区间,左闭右开) 右边记录行锁

普通索引

精确匹配,且命中

行锁 + gap lock (上一个记录和下个记录区间,左闭右开,左边记录非行锁)

普通索引

精确匹配,未命中

gap lock

普通索引

范围查询

next key lock






















4. 锁冲突


上面介绍了不同场景下会产生什么样的锁,但是看完之后会有一个疑问,针对行锁其他会话竞争的时候,可以按照X/S锁的规则来,但是这个GAP LOCK貌似只针对insert有效,insert除了加X锁之外是不是还有其他的特殊逻辑?


4.1 插入意向锁


插入意向锁其实是一种特殊的 gap lock,但是它不会阻塞其他锁。假设存在值为 4 和 7 的索引记录,尝试插入值 5 和 6 的两个事务在获取插入行上的排它锁之前使用插入意向锁锁定间隙,即在(4,7)上加 gap lock,但是这两个事务不会互相冲突等待;但是如果这个区间存在gap lock,则会被阻塞;如果多个事务插入相同数据导致唯一冲突,则在重复的索引记录上加读锁


简单来说,它的属性为:


  • 它不会阻塞其他任何锁;
  • 它本身仅会被 gap lock 阻塞

其次一个重要知识点:

  • 通常insert语句,加的是行锁,排它锁
  • 在insert之前,先通过插入意向锁,判断是否可以插入(仅会被gap lock阻塞)
  • 当插入唯一冲突时,在重复索引上添加读锁
  • 原因如下:
  • 事务1 插入成功未提交,获取了排它锁,但是事务1最终可能会回滚,所以其他重复插入事务不应该直接失败,这个时候他们改为申请读锁(疑问点:为什么要改成读锁呢?)


4.2 锁冲突矩阵


简单版矩阵

共享锁(S) 排他锁(X)
共享锁(S) 兼容 冲突
排他锁(X) 冲突 冲突


当我们将gap lock(间隙锁), next key lock(next-key锁), Insert Intention lock(插入意向锁)也加入矩阵时,就会复杂很多了


行:待加锁;列:存在锁 S(not gap) S(gap) S(next key) X(not gap) X(gap) X(next key) Insert Intention
S(not gap) - - - 冲突 - 冲突 -
S(gap) - - - - - - 冲突
S(next-key) - - - 冲突 - 冲突 冲突
X(not gap) 冲突 - 冲突 冲突 - 冲突 -
X(gap) - - - - - - 冲突
X(next-key) 冲突 - 冲突 冲突 - 冲突 冲突
Insert Intention - 冲突 冲突 - 冲突 冲突 -


说明


  • not gap: 行锁
  • gap: gap lock
  • next-key: gap + 行锁


小结


针对上面的矩阵,理解下面几个原则即可推导上面矩阵


  • gap lock只会与插入意向锁冲突
  • X行锁会与行锁冲突
  • next key lock: 行锁 + gap锁
  • 锁区间内,插入冲突;
  • 行锁的X锁冲突


II. 并发插入死锁分析



上面属于基本知识点,接下来我们看一个实际导致死锁的case


  • 并发插入相同记录导致死锁


0. 表准备


创建一个最简单最基础的表,用于演示


CREATE TABLE `t` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;
INSERT INTO `t` (`id`) VALUES (1);
复制代码


1. 事务回滚的死锁问题


场景复现:


step1:

-- session1: 
begin; insert into t values (2);
-- session2:
begin; insert into t values (2);
-- 阻塞
-- session3:
begin; insert into t values (2);
-- 阻塞
复制代码


step2:

-- session1:
rollback;
复制代码

image.png


原因分析:


死锁日志查看

SHOW ENGINE INNODB STATUS;
复制代码

image.png

  • step1:
  • session1: 插入(id=2),会添加一个X + Next Lock
  • session2/3: 插入(id=2),插入意向锁被阻塞,改为持有S + Next Lock
  • step2:
  • session1: 回滚,释放X锁
  • session2/3: 竞争X锁,只有对方释放S锁,才能竞争成功;相互等待,导致死锁


2. delete导致死锁问题


和前面操作基本一致,只是第一个会话是删除记录


step1:

-- session1: 
begin; delete from t where id=1;
-- session2:
begin; insert into t values (1);
-- 阻塞
-- session3:
begin; insert into t values (1);
-- 阻塞
复制代码


step2:

-- session1:
commit;
复制代码

image.png

原因分析和前面基本一致

image.png


3. insert加锁逻辑


insert中对唯一索引的加锁逻辑


  1. 先做UK冲突检测,如果存在目标行,先对目标行加S Next Key Lock(该记录在等待期间被其他事务删除,此锁被同时删除)
  2. 如果1成功,对对应行加X + 插入意向锁
  3. 如果2成功,插入记录,并对记录加X + 行锁(有可能是隐式锁)


根据上面这个的逻辑,那么就会有一个有意思的死锁场景


step1:

-- session1
begin; delete from t where id = 1;
-- session2
begin; delete from t where id = 1;
复制代码


step2:

-- session1
insert into t values(1)
复制代码

image.png


对应的死锁日志

image.png

关于这个场景详情博文可以参考: 记录一次Mysql死锁排查过程


4. 怎么避免死锁呢?


  • 将大事务拆成小事务
  • 添加合理的索引,走索引避免为每一行加锁,降低死锁的概率
  • 避免业务上的循环等待(如加分布式锁之类的)
  • 降低事务隔离级别(如RR -> RC 当然不建议这么干)
  • 并发插入时使用replace/on duplicate也可以避免死锁


相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
19天前
|
存储 关系型数据库 MySQL
MySQL MVCC全面解读:掌握并发控制的核心机制
【10月更文挑战第15天】 在数据库管理系统中,MySQL的InnoDB存储引擎采用了一种称为MVCC(Multi-Version Concurrency Control,多版本并发控制)的技术来处理事务的并发访问。MVCC不仅提高了数据库的并发性能,还保证了事务的隔离性。本文将深入探讨MySQL中的MVCC机制,为你在面试中遇到的相关问题提供全面的解答。
55 2
|
28天前
|
缓存 关系型数据库 MySQL
MySQL并发支撑底层Buffer Pool机制详解
【10月更文挑战第18天】在数据库系统中,磁盘IO操作是性能瓶颈之一。为了提高数据访问速度,减少磁盘IO,MySQL引入了缓存机制。其中,Buffer Pool是InnoDB存储引擎中用于缓存磁盘上的数据页和索引页的内存区域。通过缓存频繁访问的数据和索引,Buffer Pool能够显著提高数据库的读写性能。
77 2
|
11天前
|
SQL 算法 关系型数据库
面试:什么是死锁,如何避免或解决死锁;MySQL中的死锁现象,MySQL死锁如何解决
面试:什么是死锁,死锁产生的四个必要条件,如何避免或解决死锁;数据库锁,锁分类,控制事务;MySQL中的死锁现象,MySQL死锁如何解决
|
14天前
|
存储 Oracle 关系型数据库
[MySQL]知识点
本文详细介绍了MySQL中int族和char族数据类型的特点、存储范围及使用建议,以及text、blob类型和内置字符处理函数。文章强调了数据类型选择的重要性,并提供了多个实例帮助理解。
30 0
[MySQL]知识点
|
19天前
|
存储 关系型数据库 MySQL
MySQL MVCC深度解析:掌握并发控制的艺术
【10月更文挑战第23天】 在数据库领域,MVCC(Multi-Version Concurrency Control,多版本并发控制)是一种重要的并发控制机制,它允许多个事务并发执行而不产生冲突。MySQL作为广泛使用的数据库系统,其InnoDB存储引擎就采用了MVCC来处理事务。本文将深入探讨MySQL中的MVCC机制,帮助你在面试中自信应对相关问题。
56 3
|
21天前
|
关系型数据库 MySQL 数据库
一个 MySQL 数据库死锁的案例和解决方案
本文介绍了一个 MySQL 数据库死锁的案例和解决方案。
33 3
|
26天前
|
存储 关系型数据库 MySQL
优化 MySQL 的锁机制以提高并发性能
【10月更文挑战第16天】优化 MySQL 锁机制需要综合考虑多个因素,根据具体的应用场景和需求进行针对性的调整。通过不断地优化和改进,可以提高数据库的并发性能,提升系统的整体效率。
47 1
|
25天前
|
监控 关系型数据库 MySQL
MySQL并发控制与管理
【10月更文挑战第17天】MySQL并发控制与管理
22 0
|
25天前
|
存储 监控 关系型数据库
MySQL并发控制与管理:优化数据库性能的关键
【10月更文挑战第17天】MySQL并发控制与管理:优化数据库性能的关键
108 0
|
26天前
|
存储 关系型数据库 MySQL
MySQL锁,锁的到底是什么?
【10月更文挑战第16天】MySQL 锁锁定的是与数据和资源相关的对象,其目的是为了保证数据的一致性、避免冲突,并在并发环境下合理协调事务或操作的执行。理解锁的对象和意义对于优化数据库性能、处理并发问题至关重要。
54 0