暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

MySQL 核心模块揭秘 | 32 期 | 插入记录,主键索引冲突加什么锁?

72

作者:操盛春,爱可生技术专家,公众号『一树一溪』作者,专注于研究 MySQL 和 OceanBase 源码。
爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。




本文基于 MySQL 8.0.32 源码,存储引擎为 InnoDB。

目录

  • 1. 准备工作

  • 2. 加锁情况

  • 3. 原理分析

  • 4. 总结

正文

1. 准备工作

创建测试表:

CREATE TABLE `t1` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `i1` int DEFAULT '0',
  PRIMARY KEY (`id`USING BTREE,
  KEY `idx_i1` (`i1`)
ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

插入测试数据:

INSERT INTO `t1` (`id``i1`VALUES
(10101), (20201), (30301), (40401);

2. 加锁情况

t1 表中已经有一条 <id = 10> 的记录,我们执行以下 insert 语句,再插入一条 <id = 10> 的记录。

begin;
insert into t1(id, i1) values (101010);

因为新插入记录和表中原有记录存在主键冲突,执行 insert 语句之后,报错如下:

(1062, "Duplicate entry '10' for key 't1.PRIMARY'")

执行以下 select 语句查询加锁情况:

select
   engine_transaction_id, object_name,
   lock_type, lock_mode, lock_status, lock_data
 from performance_schema.data_locks
 where object_name = 't1'
 and lock_type = 'RECORD'\G

***************************[ 1. row ]***************************
engine_transaction_id | 247910
object_name           | t1
lock_type             | RECORD
lock_mode             | S,REC_NOT_GAP
lock_status           | GRANTED
lock_data             | 10

lock_data = 10, lock_mode = S,REC_NOT_GAP 表示对 <id = 10> 的记录加了共享普通记录锁。

3. 原理分析

insert 语句执行过程中,插入记录到主键索引之前,需要先找插入记录的目标位置。

目标位置为表中主键字段值小于等于新插入记录中主键字段值的最后一条记录之后。

以示例 SQL 为例,插入主键字段 <id = 10> 的记录。

插入记录到主键索引之前,先找到表中 id 小于等于 10 的最后一条记录,也就是 <id = 10, i1 = 101>
这条记录。新插入记录的目标位置就是这条记录之后。

InnoDB 发现表中已经有一条 <id = 10> 的记录,现在又要插入一条 <id = 10> 的记录,可是主键索引中不允许存在重复记录,这可怎么办才好?

直接报错吗?

那样简单粗暴就过于武断了。

InnoDB 还需要对表中 <id = 10> 的记录验明正身,确定它是一条有效的记录。

如果表中 <id = 10> 的记录已经被其它已提交事务标记删除,只是还没有被清理,它就不是有效的记录了。这种情况下,新记录可以正常插入,不会报错。

否则,新记录和表中已有记录冲突,不能插入,就可以报错了。

为了防止其它事务更新或者删除这条记录,检查表中记录是否有效之前,InnoDB 会对这条记录加共享普通记录锁。

这就是示例 SQL 执行过程中对 <id = 10> 的记录加共享普通记录锁的原因。

如果表中 <id = 10> 的记录已经被标记删除,但是删除这条记录的事务还没有提交怎么办?

那我们看到的加锁情况就不一样了。

我们可以模拟下这个场景,创建 2 个 MySQL 连接,分别执行 delete 语句和 insert 语句。

-- 连接 1(事务 1)
begin;
delete from t1 where id = 10;

-- 连接 2(事务 2)
begin;
insert into t1(id, i1) values (101010);

然后执行以下 select 语句查看加锁情况:

select
   engine_transaction_id, object_name,
   lock_type, lock_mode, lock_status, lock_data
 from performance_schema.data_locks
 where object_name = 't1'
 and lock_type = 'RECORD'\G

***************************[ 1. row ]***************************
engine_transaction_id | 247916
object_name           | t1
lock_type             | RECORD
lock_mode             | S,REC_NOT_GAP
lock_status           | WAITING
lock_data             | 10
***************************[ 2. row ]***************************
engine_transaction_id | 247911
object_name           | t1
lock_type             | RECORD
lock_mode             | X,REC_NOT_GAP
lock_status           | GRANTED
lock_data             | 10

事务 247911 执行删除操作对 <id = 10> 的记录加了排他普通记录锁。

事务 247916 想要对 <id = 10> 的记录加共享普通记录锁被阻塞,进入等待状态。

4. 总结

没有需要总结的内容了。

但是有两个问题:事务 247911 提交或者回滚之后,加锁情况是什么样的?为什么会这样?

欢迎大家评论区留言交流,作者会在 48h 内选出两条精彩留言,送出《大智小技》系列图书任意一本或《大智小技 VI》(待换券一张


往期回顾

31 期 | 隐式锁

30 期 | 死锁日志详解

29 期 | 授予锁

28 期 | 什么时候释放锁?

27 期 | 死锁(3)解决死锁

26 期 | 死锁(2)发现死锁

25 期 | 死锁(1)准备工作

24 期 | 锁等待超时

23 期 | 锁等待

22 期 | 行锁 (2) 慢速加锁

21 期 | 行锁 (1) 快速加锁

20 期 | 怎么加表锁?

19 期 | 锁模块里有什么?什么样?

18 期 | 锁在内存里长什么样?

17 期 | InnoDB 有哪几种行锁?

16 期 | InnoDB 表锁

15 期 | 事务模块小结

14 期 | 回滚整个事务

13 期 | 回滚到 savepoint

12 期 | 创建 savepoint

11 期 | InnoDB 提交事务,提交了什么?

10 期 | binlog 怎么写入日志文件?

09 期 | 二阶段提交 (3) flush、sync、commit 子阶段

08 期 | 二阶段提交 (2) commit 阶段

07 期 | 二阶段提交 (1) prepare 阶段

06 期 | 事务提交之前,binlog 写到哪里?

05 期 | 读事务和只读事务的变形记

04 期 | 终于要启动事务了

03 期 | 我是一个事务,请给我一个对象

02 期 | BEGIN 语句会马上启动事务吗?

01 期 | 事务的起源:事务池和管理器的初始化



以下是作者的个人公众号和联系方式,欢迎交流。


公众号一树一溪微信csch52

文章转载自爱可生开源社区,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论