MySQL 并发insert 导致的死锁

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 一 前言死锁,其实是一个很有意思也很有挑战的技术问题,大概每个DBA和部分开发同学都会在工作过程中遇见 。关于死锁我会持续写一个系列的案例分析,希望能够对想了解死锁的朋友有所帮助。二 案例分析2.1 业务场景正常的业务逻辑是用户触发行为系统发消息然后写入2条记录。如果遇到处理失败或者消息超时则会重试...

一 前言

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

二 案例分析

2.1 业务场景

正常的业务逻辑是用户触发行为系统发消息然后写入2条记录。如果遇到处理失败或者消息超时则会重试。

2.2 环境说明

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

CREATE TABLE `dl` (
  `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 dl(num,val) values(10,'1'),(20,'2'),(30,'30'),
(50,'50'),(60,'60');

2.3 测试用例

T1 sess1 insert into dl(num,val) values(102,'sess1');

T2 sess2 insert into dl(num,val) values(102,'sess2');

T3 sess1 insert into dl(num,val) values(101,'sess1');

T4 sess2 deadlock 

2.4 死锁日志

2018-12-30 11:45:10 0x7f61a5db9700
*** (1) TRANSACTION:
TRANSACTION 47631331, ACTIVE 7 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1
MySQL thread id 239396, OS thread handle 140057402881792, query id 82403444 127.0.0.1 root update
insert into dl(num,val) values(102,'sess2')
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 28 page no 4 n bits 80 index num_index of table `test`.`dl` trx id 47631331 lock mode S waiting
*** (2) TRANSACTION:
TRANSACTION 47631326, ACTIVE 22 sec inserting, thread declared inside InnoDB 5000
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 2
MySQL thread id 239358, OS thread handle 140057371186944, query id 82403598 127.0.0.1 root update
insert into dl(num,val) values(101,'sess1')
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 28 page no 4 n bits 80 index num_index of table `test`.`dl` trx id 47631326 lock_mode X locks rec but not gap
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 28 page no 4 n bits 80 index num_index of table `test`.`dl` trx id 47631326 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死锁问题深入剖析 )。

本案例中涉及到一个知识点 有唯一键约束,insert之前,其他事务且对即将插入的记录的next-record加了Gap-Lock 比如,程序执行insert 101之前,其他会话对102 加上了 Gap Lock 会是什么样呢?因为RC模式下 select for update 不能直接加gap lock,测试用例采用唯一键冲突的方式构造S 

sess1 insert num=28

sess1 11:10:04 >insert into dl(num,val) values(28,'23');
Query OK, 1 row affected (0.00 sec)

sess2 insert num=28 ,构造唯一键冲突,sess2 会申请在num=28记录上申请GAP Lock (记录之前还是之后呢?)

sess2 11:11:50 >insert into dl(num,val) values(28,'sess1');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

sess3 insert num=29 ,num=27 两条记录,29可以成功插入,但是num=27 则需要等待sess2的lock mode S waiting

sess3 11:12:34 >insert into dl(num,val) values(29,'sess3');
Query OK, 1 row affected (0.00 sec)

sess3 11:12:48 >insert into dl(num,val) values(27,'sess3');
Query OK, 1 row affected (11.88 sec)

三个事务相关的锁等待日志

---sess3, ACTIVE 60 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 6 lock struct(s), heap size 1136, 5 row lock(s), undo log entries 4
MySQL thread id 270616, OS thread handle 140051476178688, query id 83929304 127.0.0.1 root update
insert into dl(num,val) values(27,'sess3')
-- TRX HAS BEEN WAITING 4 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 28 page no 4 n bits 80 index num_index of table `test`.`dl` trx id 47631351 lock mode S waiting

---sess1 47631350, ACTIVE 142 sec
2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1
MySQL thread id 270559, OS thread handle 140057337198336, query id 83929372 127.0.0.1 root starting
show engine innodb status
---sess2, ACTIVE 218 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 5 row lock(s), undo log entries 1
MySQL thread id 270543, OS thread handle 140057370986240, query id 83928936 127.0.0.1 root update
insert into dl(num,val) values(28,'sess1')
TRX HAS BEEN WAITING 23 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 28 page no 4 n bits 80 index num_index of table `test`.`dl` trx id 47631349 lock mode S waiting
划重点  insert 的流程(有唯一索引的情况): 比如insert N
  1. 找到大于N的第一条记录M,以及前一条记录P

  2. 如果M上面没有gap/next-key lock,进入第三步骤,否则等待(对其next-rec加insert intension lock,由于有gap锁,所以等待)

  3. 检查P: 判断P是否等于N:

 如果不等: 则完成插入(结束)
 如果相等: 再判断P是否有锁,
    a 如果没有锁:报1062错误(duplicate key),说明该记录已经存在,报重复值错误 
    b 加S-lock,说明该记录被标记为删除, 事务已经提交,还没来得及purge
    c 如果有锁: 则加S-lock,说明该记录被标记为删除,事务还未提交.
该结论引自: http://keithlan.github.io/2017/06/21/innodb locks algorithms/

2.6 分析死锁日志

结合上面的理论和案例分析:

T1 sess1 插入num=102,并持有该记录的record lock T2 sess2 插入num=102,由于sess1已经插入但未提交,申请lock mode S waiting等待sess1释放锁。 T3 sess1 插入num=101,由于sess2 在102上申请gap lock锁住(60,102)区间故发生等待,显示lock_mode X locks gap before rec insert intention waiting

至此sess2 对num=102申请S-lock 等待sess1释放,sess1 申请insert intention waiting 等待sess2 释放gap lock,构成循环等待。

2.7 解决方法

  调整insert 101,102的顺序。

三 小结

本次死锁案例的核心点在于有唯一键约束,insert之前,其他事务且对即将插入的记录的next-record加了Gap-Lock 则需要等待。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器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
|
1月前
|
SQL 关系型数据库 MySQL
MySQL死锁及源码分析!
MySQL死锁及源码分析!
MySQL死锁及源码分析!
|
1月前
|
SQL 关系型数据库 MySQL
案例剖析:MySQL唯一索引并发插入导致死锁!
案例剖析:MySQL唯一索引并发插入导致死锁!
案例剖析:MySQL唯一索引并发插入导致死锁!
|
1月前
|
SQL 关系型数据库 MySQL
案例剖析,MySQL共享锁引发的死锁问题!
案例剖析,MySQL共享锁引发的死锁问题!
|
11天前
|
SQL 算法 关系型数据库
面试:什么是死锁,如何避免或解决死锁;MySQL中的死锁现象,MySQL死锁如何解决
面试:什么是死锁,死锁产生的四个必要条件,如何避免或解决死锁;数据库锁,锁分类,控制事务;MySQL中的死锁现象,MySQL死锁如何解决
|
19天前
|
存储 关系型数据库 MySQL
MySQL MVCC深度解析:掌握并发控制的艺术
【10月更文挑战第23天】 在数据库领域,MVCC(Multi-Version Concurrency Control,多版本并发控制)是一种重要的并发控制机制,它允许多个事务并发执行而不产生冲突。MySQL作为广泛使用的数据库系统,其InnoDB存储引擎就采用了MVCC来处理事务。本文将深入探讨MySQL中的MVCC机制,帮助你在面试中自信应对相关问题。
57 3
|
21天前
|
关系型数据库 MySQL 数据库
一个 MySQL 数据库死锁的案例和解决方案
本文介绍了一个 MySQL 数据库死锁的案例和解决方案。
33 3
|
26天前
|
存储 关系型数据库 MySQL
优化 MySQL 的锁机制以提高并发性能
【10月更文挑战第16天】优化 MySQL 锁机制需要综合考虑多个因素,根据具体的应用场景和需求进行针对性的调整。通过不断地优化和改进,可以提高数据库的并发性能,提升系统的整体效率。
47 1
|
1月前
|
SQL 关系型数据库 MySQL
MySQL 是怎么做并发控制的?
本文以 MySQL 8.0.35 的代码为例,尝试对 MySQL 中的并发访问控制进行一个整体的介绍。