MySQL--万文长字探究隔离性实现原理

news2024/11/23 3:25:52

1 隔离性简介

事务具有原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)四个特性,简称 ACID,缺一不可。这篇文章旨在讲清楚隔离性的产生背景、有何作用及实现原理等。

1.1 产生背景

并发环境下保证共享资源的线程安全性。

数据库表中的一行行数据就是共享资源,链接至数据库服务端的众多客户端就好比是多线程环境。

1.2 作用

保证数据库表数据,在每个事务间是读写安全,写写安全的。

2 MySQL事务间的读写冲突 & 解决方案

事务间的读写冲突,分为三种情况:脏读、不可重复读、幻读

2.1 脏读

脏读指当前事务能够读到其他事务未提交的数据,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中。读到了不一定最终存在的数据,这就是脏读。

2.2 不可重复读

不可重复读指在同一事务内,不同时刻读到的同一批数据,结果显示可能不一样。原因是这批数据可能会受到其他事务的影响,比如其他事务改了这批数据并提交了。

2.3 幻读

MySQL 官方给出了幻读的定义:在同一事务中,相同的 SELECT 语句,得到的结果不一致(又造成了不可重复读,破坏了可重复读的隔离级别),并且第二次 select 的 raws 只会比第一次 select 的 raws 要多,这些多出的行,被称为“幻行”(即幻读只会发生在新增数据的场景下)

if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a“phantom”row.

关于幻读,还有一道很经典的面试题:InnoDB RR 隔离级别下,是否解决了幻读?

这里先给出结论:快照读场景下,利用MVCC规避了大部分幻读场景,当前读场景下,利用间隙锁解决了幻读。事实上,快照读场景下,MVCC无法规避的幻读场景,在实际应用中也很少出现,因为一般没人那样写SQL,因此可以理解为MVCC + 间隙锁完全解决了幻读问题。

因为幻读的产生及解决方式涉及到 MVCC、快照读、当前读及间隙锁等背景知识,所以这部分的详细内容在下面“再探幻读”一节还会阐述。


SQL 标准定义了四种隔离级别用于解决上述这些读写冲突,MySQL 全都支持,下面一一分析。(注:从上往下,隔离强度逐渐增强,性能逐渐变差。采用哪种隔离级别要根据系统需求权衡决定,其中,可重复读是 MySQL 的默认级别。)

2.4 读未提交

没有解决上述任何一种读写冲突,即 MySQL 面对事务间的读写冲突没有做任何事情,性能最好,也最不实用。

2.5 读已提交

解决了脏读的问题,即当前事务只能读到其他事务已经提交过的数据。

读已提交是大多数流行数据库的默认隔离级别,比如 Oracle,但不是 MySQL 的默认隔离级别。

2.6 可重复读

解决了脏读,不可重复读的问题,即当前事务不会读到其他事务对同一批数据的修改,即使其他事务已提交。但是,对于其他事务新插入的数据是可以读到的,这也就引发了幻读问题。

2.7 串行化

解决了脏读,不可重复读,幻读的问题,它将事务的执行变为顺序执行,后一个事务的执行必须等待前一个事务结束,虽然隔离效果最好,但性能最差。

2.8 总结

隔离级别脏读不可重复读幻读
读未提交可能可能可能
读已提交不可能可能可能
可重复读不可能不可能可能(MVCC无法规避的某些场景)
串行化不可能不可能不可能

3 MVCC-MySQL解决读写冲突的实现原理

我们平时在做开发,若在多线程环境下想要保证线程安全,大多数的解决方式都是加锁–同步锁、读写锁等。MySQL 在解决读写冲突时也用到了锁,但又不仅仅只用到了锁,需要分情况讨论。

对于同一条数据库记录来说:

  • 读未提交,性能最好,因为它压根儿就不加锁,所以可以理解为事务间毫无隔离性
  • 串行化,性能最差,可以理解为事务运行时使用了排它锁,多个事务间调度必须串行执行

这两种隔离级别在实际中几乎没人使用,读未提交的线程安全性毫无保证,串行化则性能太差。

同时,MySQL 能够支持众多不同业务下的高并发场景需求,因此如果使用锁机制实现事务隔离性,则读写性能可能还是会差强人意。因此,为了提⾼数据库的并发性能,⽤更好的⽅式去处理读-写冲突,做到即使有读写冲突,也不会加锁,MVCC(Multi-Version Concurrency Control) 即多版本并发控制诞生了。

MVCC 除了提高数据库的读写性能外,还解决了脏读、不可重复读、幻读等问题。但是不能解决写写冲突下的更新丢失问题。

MVCC 的实现原理主要依赖记录中的隐式字段,undo 日志,readview 等,接下来会一一介绍。不过在此之前,先要了解一下什么是当前读和快照读:

  • 当前读:select lock in share mode(共享锁)、select for updateupdateinsertdelete这些操作都是一种当前读,它会读取记录的最新版本,并对读取的记录进行加锁,保证其他并发事务不能修改当前记录
  • 快照读:select操作就是快照读,即不加锁的非阻塞读,快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;快照读可能读到的并不一定是数据的最新版本,而是之前的历史版本

实际上 MVCC 只是一个抽象概念,即:维持一个数据的多个版本,使得读写操作没有冲突,并非实现。快照读相当于是 MySQL 对 MVCC 模型中非阻塞读功能的实现。相对而言,当前读则是悲观锁的具体功能实现。

3.1 隐式字段

数据表中的每条记录,除了我们自定义的字段外,还有数据库隐式定义的 DB_TRX_ID, DB_ROLL_PTR, DB_RAW_ID 等3个字段。

  • DB_TRX_ID:最近修改(更新、插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID
  • DB_ROLL_PTR:回滚指针,配合 undo 日志,指向这条记录的上一个版本
  • DB_RAW_ID:隐藏主键(自增id),如果数据表没有主键,则 InnoDB 会自动以 DB_ROW_ID 产生一个聚簇索引

例,person 表的某条记录:

image.png

3.2 undo log

undo log 实质分两种:

  • insert undo log:事务在 insert 记录时产生的 undo log,只在回滚时需要,并在事务提交后立即丢弃
  • update undo log:事务在 update 或 delete 记录时产生的 undo log,不仅事务回滚时需要,快照读时也需要,不能随便删除,只有在当前读或事务回滚不涉及该日志时,才会被 purge 线程统一清理

对 MVCC 有实质帮助的是 update undo log。

3.3 undo log的生成流程

举个例子:

1.有个事务向 person 表插入了一条新纪录,隐式主键是1,回滚指针和事务ID假设是 NULL,如下:

image.png

2.事务1将该记录的 name 改为 tom:

  • 事务1修改该行(记录)数据时,数据库会先对该行加排他锁
  • 然后把该行数据拷贝到 undo log 中作为旧记录,即在 undo log 中有当前行的拷贝副本
  • 拷贝完毕后,修改该行 name 为 Tom,并且修改隐藏字段的事务 ID 为当前事务1的 ID(事务 ID 默认从1开始递增),回滚指针指向拷贝到 undo log 的副本记录,即表示该行数据的上一个版本就是它
  • 事务提交后,释放锁

修改成功后,该行数据的组织形式如下:

image.png

3.事务2继续修改该行记录,将 age 修改为 30 岁:

  • 同样,数据库也先为该行加锁
  • 然后把该行数据拷贝到 undo log 中作为旧记录,发现该行记录已经有 undo log 了,那么最新的旧数据将作为链表的表头,插在该行记录的 undo log 的最前面
  • 修改该行 age 为 30 岁,并且修改隐藏字段的事务 ID 为当前事务2的 ID,也就是 2,回滚指针指向刚刚拷贝到 undo log 的副本记录
  • 事务提交,释放锁

最终该行数据的组织形式如下:

image.png

如上述过程,不同事务或相同事务对同一记录的修改,会导致该记录的undo log成为一条记录版本的链表,undo log 的链首就是最新的旧记录,链尾就是最早的旧记录。

3.4 readview读视图

Read View 就是事务进行快照读时生产的读视图(Read View),即数据库系统当前的一个快照,读视图会记录并维护系统当前活跃事务的 ID(当每个事务开启时,都会被分配一个递增 ID,所以越新的事务,ID 值越大)。

Read View 主要用来做可见性判断,即当某个事务执行快照读的时候,会对该记录创建一个读视图,把它比作条件来判断当前事务能够看到该行记录哪个版本的数据,即可能是最新的数据,也有可能是 undo log 里某个版本的数据。

Read View 有三个全局属性:

  1. trx_list:一个数值列表,维护生成 Read View 时系统正活跃的事务 ID 列表
  2. low_limit_id:trx_list 列表中最小的事务 ID
  3. up_limit_id:ReadView 生成时系统尚未分配的下一个事务 ID,即已出现过事务 ID 的最大值 + 1

Read View 的可见性算法:将当前的事务 ID、产生快照读的记录的最新事务 ID(DB_TRX_ID)及 readview 维护的事务 ID 取出来,进行比较,如果DB_TRX_ID不符合可见性,则通过DB_ROLL_PTR回滚指针取出Undo Log中上一版本的DB_TRX_ID再比较,即遍历链表的DB_TRX_ID,直到找到满足特定条件的DB_TRX_ID,那么这条记录就是当前事务所能看见的版本。

可见性算法的规则如下:

  1. 比较产生快照读的记录的最新事务 ID(DB_TRX_ID)与 low_limit_id, 如果 DB_TRX_ID < low_limit_id,说明此版本的记录对应的事务已提交,则当前事务能看到DB_TRX_ID对应的版本记录,如果大于等于则进入下一个判断
  2. 如果 DB_TRX_ID >= up_limit_id,则代表DB_TRX_ID所在的记录是在 Read View 生成后才出现的,因此对当前事务不可见,如果小于则进入下一个判断
  3. 如果DB_TRX_ID >= low_limit_id && DB_TRX_ID < up_limit_id,则判断trx_list.contains (DB_TRX_ID)(即 DB_TRX_ID 是否在活跃事务之中)。如果在,则代表 Read View 生成时,该记录版本对应的事务还在活跃,没有 Commit,因此此记录版本当前事务不可见;如果不在,则说明,此版本记录对应的事务在 Read View 生成之前就已经 Commit 了,因此当前事务可见
  4. 如果最新记录版本不满足事务可见性,则遍历 undo 日志链表,取更早的记录版本,重复上述判断规则,直到找到可见的版本记录为止

从可见性算法的实现规则来看,MVCC 解决了脏读的问题,即实现了读已提交。

3.5 整体流程

在了解了 MVCC 的基本实现原理后,我们举个例子,再来模拟一下 MVCC 的整体流程。

1.事务 2 对某行数据执行了快照读,数据库为该行数据生成一个 Read View 读视图,假设当前事务 ID 为 2,同时还有事务1和事务3在活跃中,事务4在事务2快照读前一刻提交了更新。Read View 记录了系统当前活跃的事务ID——1,3,维护在 trx_list 上。

事务1事务2事务3事务4
事务开始事务开始事务开始事务开始
修改且已提交
进行中快照读进行中

2.Read View 除了维护 trx_list 列表,还有两个属性:low_limit_id、up_limit_id。在这个例子中,low_limit_id 就是 1,up_limit_id 就是 4 + 1 = 5,trx_list 集合的值是 1、3。Read View 如下图:

mvcc变量.png

3.在此例子中,只有事务4修改过该行记录,并在事务2执行快照读前就提交了事务,所以当前该行数据的undo log如下图所示;事务2在快照读的时候,会拿该行记录的 DB_TRX_ID 去跟 up_limit_id,low_limit_id 和 trx_list 比较,以此判断事务2能看到该记录的版本是哪个

image.png

4.判断过程如下:

  • 先拿该记录最新的 DB_TRX_ID(即4)跟 Read View 的 low_limit_id 比较,是否满足DB_TRX_ID(4) < low_limit_id(1),不符合条件,继续下一步判断
  • 判断DB_TRX_ID(4) >= low_limit_id(5),也不符合条件,继续下一步判断
  • 最后判断trx_list.contains(4),发现事务4并不在当前活跃事务列表中,即 Read View 生成时事务4已提交,符合可见性条件,因此事务2能读到事务4所提交的记录版本

MVCC 可见性算法的整体流程如下:

image.png

3.6 RR隔离级别的实现

事务A事务B
开启事务开启事务
快照读查询金额为500快照读查询金额为500
更新金额为400
提交事务
快照读金额为?
select lock in share mode当前读金额为400

如上图,在 RC 隔离级别下,事务 B 进行快照读的结果为 400,在 RR 隔离级别下,结果为 500。

Read View 的使用方式,造成 RC、RR 级别下快照读的结果不同。

  • RC 级别:每次快照读都会新生成一个 Read View,因此可能导致同一事务中前后读取同一条数据的结果并不相同
  • RR 级别:在整个事务中,只会记录产生的第一个 Read View,此后对数据进行快照读的时候,使用的是同一个 Read View,因此对 Read View 生成后的修改不可见

3.7 再探幻读

3.7.1 幻读的再解释

为了加深大家对 2.3 小结中幻读定义的印象,我举下面几个例子:

例子一

image.png

如上图,步骤 6 读到了另一个事务新增的数据,这个不是幻读,幻读是违背了可重复读的定义(同一个 select 语句,当前读的 select 与快照读的 select 还是不一样的),即如果步骤 5 所读到的数据与步骤 2 不一致,才叫幻读。

例子二

image.png

如上图,上图描述有误,RR 隔离级别下,根据 MVCC 读视图的生成规则,由于事务 A 中的第二个 Select 语句使用的 Read View 与第一个 Select 语句是一样的,因此事务 B 插入的数据不符合可见性原则,因此也不会发生幻读。

例子三

image.png

如上图,id 列是主键,由于事务 B 已经插入了id=30的数据,因此即使事务 A 两次快照读都没有读到id=30的数据,步骤 6 还是会插入失败。这个也不是幻读,为了满足 RR 隔离级别,这个现象是正常的,符合 SQL 标准。

3.7.2 MVCC无法规避的幻读场景

3.7.1小节中同时说明了那些本应该出现,但 MVCC 已经规避了的幻读场景,下面看两个 MVCC 无法规避的幻读场景。

image.png

可以看到,有两个事务 session1 与 session2。session1 在 session2 插入数据前后的两次快照读,读到的结果都为空(即使 session2 提交事务的时机放在 session1 第二次快照读之前,结果也依然为空),符合 RR 隔离级别。但是在 session1 进行了update t1 set a = 3之后,再次快照读,发现多出了一条数据(破坏了 RR)。这条数据,就是幻行(session 1 本身没有新增数据,因此 RR 隔离级别下,session1 中相同的快照读语句,前后读到的结果本应该是一致的)。

相同的例子,还有如下:

image.png

以上述这个例子说明这部分MVCC无法规避的幻读场景产生的原因:

  1. 事务 1 先 select,事务 2 insert 并 commit
  2. 事务 1 再次 select,根据 RR 隔离级别的原理,查询的结果与第一次 select 一样
  3. 接着事务 1 不加条件地 update,这个 update 会作用在所有行上,包括事务 2 新加的(新加的行的事务 id 被改变为 1,符合可见性原则)
  4. 事务 1 再次 select 就会出现事务 2 中的新行,出现幻读

快照读场景下,幻读发生的本质是由于当前事务更新了新插入数据的事务id,导致新插入数据的可见性发生变化,破坏了RR隔离级别。

不过也正如上述场景所述,在实际应用中,我们很少这样去写 SQL,更别说像例子二一样去写全表更新这样的 SQL,因此可以简单的认为,MVCC 已经解决了快照读场景下的幻读问题。

3.7.3 间隙锁-当前读下幻读的规避方案

由于当前读会给记录加锁,阻止了其他事务在当前读期间,并发修改记录的情况发生,因此当前读天然的不会出现脏读及不可重复读的问题,但是幻读仍然会发生。

例子如下:

CREATE TABLE `author` ( 
    `id` int(11) NOT NULL AUTO_INCREMENT, 
    `name` varchar(255) DEFAULT NULL, 
    `age` int(11) DEFAULT NULL, 
    PRIMARY KEY (`id`) 
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;

INSERT into author VALUES (1,'g1',20),(5,'g5',20),(15,'g15',30),(20,'g20',30);
时间session1session2
begin;
T1select * from author where age = 20 for update;
T2INSERT into author VALUES (25,‘g25’,20);
T3select * from author where age = 20 for update;
T4commit;

session1在 T1 时刻读取到的结果:(1,'g1',20), (5,'g5',20),若没有间隙锁,session1在 T3 时刻会出现幻读,即读取到的结果:(1,'g1',20), (5,'g5',20), (25,'g25',20),若引入了间隙锁,则 session2 在 T2 时刻的插入语句会被阻塞,直到 session1 commit 为止,这样就防止了幻读的发生。

那么间隙锁的实现原理是什么呢?看下面这个例子。

-- 建表
CREATE TABLE `userinfo` (
  `id` int(11) NOT NULL COMMENT '主键',
  `name` varchar(255) DEFAULT NULL COMMENT '姓名',
  `age` int(11) DEFAULT NULL COMMENT '年龄,普通索引列',
  `phone` varchar(255) DEFAULT NULL COMMENT '手机,唯一索引列',
  `remark` varchar(255) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_userinfo_phone` (`phone`) USING BTREE COMMENT '手机号码,唯一索引',
  KEY `idx_user_info_age` (`age`) USING BTREE COMMENT '年龄,普通索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 初始化数据
INSERT INTO `userinfo`(`id`, `name`, `age`, `phone`, `remark`) VALUES (0, 'mayun', 20, '0000', '马云');
INSERT INTO `userinfo`(`id`, `name`, `age`, `phone`, `remark`) VALUES (5, 'liuqiangdong', 23, '5555', '刘强东');
INSERT INTO `userinfo`(`id`, `name`, `age`, `phone`, `remark`) VALUES (10, 'mahuateng', 18, '1010', '马化腾');
INSERT INTO `userinfo`(`id`, `name`, `age`, `phone`, `remark`) VALUES (15, 'liyanhong', 27, '1515', '李彦宏');
INSERT INTO `userinfo`(`id`, `name`, `age`, `phone`, `remark`) VALUES (20, 'wangxing', 23, '2020', '王兴');
INSERT INTO `userinfo`(`id`, `name`, `age`, `phone`, `remark`) VALUES (25, 'zhangyiming', 38, '2525', '张一鸣');

首先明确一点:行锁及间隙锁不是加在记录上的,而是加在索引上的!

如上表,userinfo中目前有 3 种索引,分别是主键索引、唯一索引、普通索引。唯一索引这里用处不大,可以先忽略。

间隙锁,顾名思义,锁的是索引之间的间隙,因此对于此表数据,间隙锁的作用范围如下(以主键为例,另外两个索引同理):

image.png

具体间隙锁是如何规避幻读的,例子如下:

select * from userinfo where age = 23 for update;

如上述 SQL,由于普通索引并不唯一,因此可能有并发事务会新增一条 age 为 23 的数据,这时如果没有间隙锁,就会发生幻读。那么在此例中,为了防止出现幻读,只要给(20,23), (23, 27)这两个区间,即(20, 27)加上间隙锁即可。

image.png

对上图再做一个详细的解释(上图涉及到 B+Tree 的结构,不清楚的可以先了解一下 B+Tree):

  1. 此时的间隙锁为(20, 27),不包含左右两边的边界值 20 和 27
  2. 在 age=23 的普通索引上有两把行锁,同时在 id=5 和 id=20 的主键索引上也有两把行锁
  3. 只要当 age 的取值范围为 (20, 27) 时,任何并发事务都不能插入成功,因为间隙锁阻止了数据的插入,当然也阻止了修改与删除
  4. 只要当 age 的值不属于 (20, 27),且不等于 20 也不等于 27 时,任何并发事务都能插入成功,前提是待插入的数据行中的 id 值符合主键唯一性和 phone 值符合唯一索引唯一性
  5. 当 age=20 的时候,只有对应的主键值小于 0 才能插入。举例说明:当待插入的数据为age=20, id=1时,还需要为 age 这个非唯一索引下面存上主键索引的值:1,此时的 1 比表中已经存在的主键值 0 大,所以它会放在 0 的后面,此时的age=20, id=1依然会占用被锁住的索引间隙,所以此时不能插入成功。能插入成功的数据为age=20, id=-1age=20, id=-2这样的组合
  6. 当 age=27 的时候,同理,只有对应的主键值大于 15 才能插入。因为主键值如果小于 15,如上图,若插入age=27, id=14,数据会被放在age=27, id=15的前面,这时依然会占用被锁住的索引间隙,所以不能插入成功
  7. 上面红色箭头的区域都是有间隙锁的区域,在此区域内新增数据不能插入成功

由于间隙锁锁的是索引之间的位置,因此会发生上述第 5、6 点的情况,但个人觉得其实并没有必要, MySQL 应该是可以准确计算出间隙锁作用的区间是开区间还是闭区间。在本例中,当 age 等于 20 或 27 时,即使 id 取任意值插入,也不会出现幻读,但 MySQL 并没有这样实现。这个东西也不必深究,只需明白间隙锁的原理即可。如下是对 5、6 点情况的验证:

image.png

4 MySQL事务间的写写冲突 & 解决方案

4.1 何为写写冲突

不过多赘述,学习过并发编程的应该都明白如果没有互斥条件,并发写可能出现的问题就是数据丢失(数据覆盖)等,并发事务同理。

这里主要关注 MySQL 对并发事务之间的写冲突如何处理,首先来了解一下 MySQL 中的各种锁。

4.2 共享锁

可以被不同事务共享的锁就是共享锁(Shared Locks),简称 S 锁、读锁。共享锁与排它锁互斥,共享锁之间不互斥。

如下 SQL 会加共享锁:

SELECT ... LOCK IN SHARE MODE;

4.3 排它锁

排它锁,也称独占锁(Exclusive Locks),简称 X 锁、写锁。排它锁之间互斥,排它锁与共享锁互斥,因此一个时刻,一行数据,只能被一个事务加 X 锁。

如下 SQL 会加排它锁:

SELECT ... FOR UPDATE;
INSERT ... ;
UPDATE ... ;
DELETE ... ;

4.4 表锁

MySQL 中粒度最大的一种锁,对整张表加锁,资源开销比行锁少,不会出现死锁,但发生锁冲突的概率很大。被大部分 MySQL 引擎支持,如 MyISAM 和 InnoDB。MyISAM 只支持表锁,InnoDB 默认是行锁,只有全表扫描的时候才会升级为表锁。因此我们主要围绕 MyISAM 来讲述一下表锁。

MySQL的表级锁有两种模式:

  • 表级共享锁:不会阻塞其它线程对同一表的读请求,但会阻塞对同一表的写请求。只有当表级共享锁释放后,才会执行其它线程的写操作
  • 表级排他锁:会阻塞其它线程对同一表的读和写操作,只有当表级排他锁释放后,才会执行其它线程的读写操作。MyISAM 下写锁默认比读锁的优先级要高,即使读请求先到达锁请求队列,也会放在写请求的后面执行

因此 MyISAM 不适合做写多读少场景下的存储引擎。想要了解更多表锁在 MyISAM 中的技术细节,可以阅读此文章:MySQL中的锁(表锁、行锁)。

4.5 页锁

MySQL 中数据在内存及磁盘上存储的基本单位并不是记录(行),而是页,可以简单理解为多个行会组成一个页,页锁即是给页上加的锁。

BDB 存储引擎中实现了页锁,而 InnoDB 以及 MyISAM 存储引擎都没有页锁,因此知道有这个概念就行,有兴趣的同学可以自行了解。

4.6 行锁

行锁大家都比较熟悉,这里不再赘述。

4.7 意向锁

意向锁是一种表锁,也分为意向共享锁(IS)和意向排它锁(IX)。由于 InnoDB 既支持表锁又支持行锁,因此意向锁的作用是用来辅助表锁和行锁的冲突判断。在取得行级共享锁和行级排它锁之前,会先取得对应的意向共享锁和意向排它锁(此步骤数据库系统自动完成,不需要人工干预)。

若没有意向锁,则判断表锁和行锁冲突的时候,需要对索引进行遍历,判断表上是否有行锁。有了意向锁,只要判断表是否有意向锁,就知道表上是否有行锁。表锁与意向锁的兼容性如下:

XIXSIS
XConflictConflictConflictConflict
IXConflictCompatibleConflictCompatible
SConflictConflictCompatibleCompatible
ISConflictCompatibleCompatibleCompatible

注:从意向锁的作用及加锁时机可知,意向锁不与行锁冲突,因此表格中的 S、X 代表的是表级共享锁与表级排它锁。

4.8 悲观锁 & 乐观锁

悲观锁 & 乐观锁都是抽象概念,并不是真实存在的锁。大部分情况下,悲观锁及乐观锁的使用需要我们自己编写业务逻辑进行实现。

悲观锁 & 乐观锁产生的背景:由于在并发场景下,共享资源也不可能时时都处于竞争状态,基于此种条件,提出了悲观锁及乐观锁。

基于产生背景,悲观锁顾名思义,即并发场景下对共享资源的操作比较悲观,认为共享资源可能时刻处于竞争冲突中,因此操作数据的时候,直接加排他锁,保证本次操作的原子性、互斥性、可见性。像 Java 中 synchronized 的使用就是悲观锁的实现场景。

而乐观锁则相反,即并发场景下对共享资源的操作比较乐观,默认每次对共享资源的操作并没有引起竞争冲突,即每次操作天然线程安全,不用加锁,只有发现确实出现了竞争冲突,才会对共享资源真正的上锁。比较有代表性的实现方案就是 CAS 与版本号方案。

乐观锁的使用场景这里不再赘述,有兴趣的可以阅读此文章-【BAT面试题系列】面试官:你了解乐观锁和悲观锁吗?,写的比较好。

这里重点提一下面试官喜欢问的两个问题(上文已有详细的描述,这里总结一下):

1.CAS的ABA问题本质是什么?怎么解决?

当业务场景需要关注共享资源被操作的中间过程,却没有进行记录,就容易引发 ABA 问题。解决方案就是引入版本号或时间戳,即记录共享资源被修改的次数或其他信息。

详情可阅读:真实业务场景展现CAS原理的ABA问题及解决方案。

2.乐观锁有加锁吗?

乐观锁本身不加锁,只是在更新时判断一下数据是否被其他线程更新了,AtomicInteger 便是一个例子。有时乐观锁可能会与加锁操作合作,例如 update test set id = 1 where version = 1 这条 SQL,使用版本号机制实现乐观锁,但 update 时 MySQL 默认还会加排它锁,但这只是乐观锁与加锁操作合作的例子,不能改变 “乐观锁本身不加锁” 这一事实。

4.9 Next-key锁

Next-key 锁也不是什么新鲜玩意,事实上,前面所讲的行锁与间隙锁的组合就并称为 Next-key 锁,之所以有这么个叫法,是因为 MySQL 中加锁的基本粒度是 Next-key 锁,即默认行锁 + 间隙锁的组合,不过不同情况下,Next-key 锁可能会退化成单独的行锁或间隙锁。

以 3.7.3 节的例子再为例,Next-key 锁的锁范围如下:

image.png

注:Next-key 锁是左开右闭的区间。

4.10 插入意向锁

插入意向锁是一个间隙锁,表示需要在该间隙插入记录。插入意向锁之间互相不排斥,例如在同一个间隙并发插入记录(这里假设只有主键索引),只要记录主键不冲突是可以并发插入成功的。但是插入意向锁与间隙锁、排他锁以及共享锁是冲突的,这也是 RR 隔离级别下 Next-key 锁控制范围内不能新增记录的机制。

插入意向锁的 LOCK_MODE:X,GAP,INSERT_INTENTION。

4.11 加解锁时机

MySQL中使用两阶段锁协议进行加解锁,详情链接:详解MySQL两阶段加锁协议。

这里做个总结:

  1. 在事务中,在对记录进行更新或者(select for update、lock in share model)时,就会对记录加锁,但只有提交(commit)或者回滚(rollback)时才会解锁
  2. 依据 2PL 的性能优化:SQL 编写时,越热点的记录离事务的终点越近,这样可以显著提高吞吐量

4.12 加锁情况详细分析

上面基本已经介绍完了 MySQL 中常见的所有锁,接下来,是本篇节最重要的部分,各种加锁情况分析(来将所学的知识融会贯通吧)~

不同的 MySQL 版本,相同的 SQL 加锁情况可能不尽相同,下面的结论只是根据前述所讲所得出的理论,并不代表实际加锁情况,况且不同的 MySQL 版本中 Next-key 的加锁范围还有一定的 bug,这里不关注这些旁枝末节。

如果想要查看 MySQL 实际的加锁情况,可使用 select * from performance_schema.data_locks 命令进行查看。此命令执行后,有几个重要关键字简单解析一下:

LOCK_TYPELOCK_MODEINDEX_NAMELOCK_DATA锁范围
TABLEIXNULLNULL表级锁,意向排他锁
RECORDX,REC_NOT_GAPPRIMARY15主键值为15的数据加了行锁
RECORDX,GAPuniq_a115, 15唯一索引列,值为115之前的间隙,不包括115加了间隙锁
RECORDXuniq_a115, 15唯一索引列,值为115之前的间隙加了 Next-key 锁,前开后闭区间,包括115

0.环境准备

MySQL 不同的版本加锁策略可能不同,下面以 MySQL 8.0.25 版本为例,进行多角度验证 next-key lock 的加锁范围。

MySQL 版本:8.0.25、隔离级别:可重复读(RR)、存储引擎:InnoDB。

CREATE TABLE `t` (
  `id` int NOT NULL COMMENT '主键',
  `a` int DEFAULT NULL COMMENT '唯一索引',
  `c` int DEFAULT NULL COMMENT '普通索引',
  `d` int DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uniq_a` (`a`),
  KEY `idx_c` (`c`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 初始化数据
INSERT INTO `t`(`id`, `a`, `c`, `d`) VALUES (0, 10, 20, 30);
INSERT INTO `t`(`id`, `a`, `c`, `d`) VALUES (5, 15, 25, 35);
INSERT INTO `t`(`id`, `a`, `c`, `d`) VALUES (10, 110, 210, 310);
INSERT INTO `t`(`id`, `a`, `c`, `d`) VALUES (15, 115, 215, 315);
INSERT INTO `t`(`id`, `a`, `c`, `d`) VALUES (20, 120, 220, 320);

1.主键索引等值查询-值存在

mysql> begin; select * from t where id = 10 for update;

直接说结论,根据前文所述,首先会给表加上意向排它锁,虽然加锁的基本单位是 Next-key,但这里主键存在,只要锁住 id=10 的主键索引就行,因此 Next-key 会退化为行锁。

2.主键索引等值查询-值不存在

mysql> select * from t where id = 11 for update;

主键索引结构如下:

image.png

根据前文所述,首先会加表级意向排它锁,然后为防止幻读,Next-key会退化为间隙锁,间隙锁范围是(10, 15)。

3.主键索引范围查询

mysql> begin; select * from t where id > 10 and id <= 15 for update;

根据索引结构以及前文所述,首先会给表加意向排它锁,然后会给(10, 15]区间加 Next-key 锁,不会退化。

4.唯一索引等值查询-值存在

begin; select * from t where a = 110 for update;

唯一索引等值查询也不需要间隙锁,原因与主键索引等值查询一样。不同的是索引上的加锁方式,由于涉及到回表,因此 X 锁不仅会加到唯一索引上,同时也会加到主键索引上。

这里牵扯到两个问题:

  1. X锁只给唯一索引上加行不行

    • 不行,如果此时有一个并发SQL:delete from t where id = 10;,而select for update语句没有将主键索引上的记录加锁,那么并发的 delete 就会感知不到前面 select 语句的存在,违背了同一记录上排他锁之间互斥的原则
  2. X锁只给主键索引上加行不行

    • 可以,由于唯一索引与主键索引是一一映射的关系,并且写操作最终会回归到主键索引上,因此是可以只给主键索引加锁的,但 MySQL 并没有这样做,我想原因大概是为了提高性能,毕竟唯一索引上只要加了锁,就有可能避免一次无效的回表操作

综上,不管是select for share语句还是不涉及回表的select id from t where a = 110 for update;语句,最终都会给主键索引加上相应的锁。

5.唯一索引等值查询-值不存在

begin; select * from t where a = 111 for update;

image.png

首先会给表加意向排他锁,然后会给唯一索引的 (110,115) 加上间隙锁(Next-key锁退化),由于已经解决了幻读问题,因此主键索引上不会加锁。

6.唯一索引范围查询

mysql> begin; select * from t where a >= 110 and a < 115 for update;

首先会给表加意向排他锁,然后 Next-key 锁会分别退化为唯一索引 110 的行锁,(110, 115)的间隙锁,由于 110 有对应的主键,因此也需要对相应的主键索引加上行锁。

6.普通索引等值查询-值存在

begin; select * from t where c = 210 for update;

首先加意向排它锁,由于是普通索引,值可能重复,因此 Next-key 锁不会退化,同时给 210 对应的主键索引也加上行锁。

7.普通索引等值查询-值不存在

begin; select * from t where c = 211 for update;

首先加意向排它锁,然后给 215 的前置区间加 Next-key 锁,由于 (210, 215) 的锁区间已经能够防止幻读产生,因此 Next-key 锁退化成间隙锁。

8.普通索引范围查询

mysql> begin; select * from t where c >= 210 and a < 215 for update;

首先加意向排它锁,由于是普通索引,索引值可能重复,因此第一个 Next-key 锁并不会退化,锁区间为(25,210],并给 210 对应的主键索引加上行锁,第二个 Next-key 锁会退化为间隙锁,锁范围为(210, 215)。

9.无索引查询

当锁定读查询语句select for shareselect for update没有使用索引列作为查询条件,或者查询语句没有走索引查询,将导致全表扫描,同时锁全表(全表的每一个索引都会加 Next-key 锁,相当于把整个表锁住了,并不是加了表锁),这时如果其他事务对该表进行写操作,都会被阻塞。

根据两阶段锁协议,以及 MySQL 加锁的基本单位是 Next-key 锁,最终全表的加锁情况将如下所示:

image.png

因此,在线上执行 update、delete、select for update 等具有加锁性质的语句时,一定要检查语句是否走了索引,如果发生了全表扫描,那将会引发很严重的问题!

5 扩展阅读

  1. MySQL RR级别依然可能丢失更新数据
  2. 字节面试:加了什么锁,导致死锁的?

6 参考阅读

  1. 【MySQL笔记】正确的理解MySQL的MVCC及实现原理
  2. MySQL可重复读级别不是也支持间隙锁吗,为什么还是无法解决当前读下的幻读? - 寇亚龙的回答 - 知乎
  3. 阿里云PolarDB-MySQL源码分析·InnoDB RR隔离级别之大不同
  4. 再探幻读!什么是幻读?为什么会产生幻读,MySQL中是怎么解决幻读的?–幻读的描述不准确
  5. MySQL事务隔离级别和实现原理–幻读的描述不准确
  6. MySQL中间隙锁的加锁机制–讲的很好
  7. MySQL锁释放时机(事务)
  8. MySQL next-key lock 加锁范围是什么?–讲的很好
  9. 看来,MySQL next-key lock 的 bug 并没有被修复!–讲的很好

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/593666.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

吐血整理 二叉树(链表实现)的基本操作详解!

文章目录 节点设置二叉树的深度优先遍历前序遍历中序遍历后序遍历 二叉树的广度优先遍历层序遍历 节点的个数叶子节点的个数第K层节点的个数值为X的节点树的最大深度翻转二叉树判断两颗二叉树是否相同判断二叉树是否是完全二叉树判断二叉树是否是单值二叉树判断二叉树是否是平衡…

有哪些好用的pdf修改器?思路提供

PDF格式的文档在现代生活中扮演着越来越重要的角色。但是&#xff0c;要编辑或修改PDF文件是一件非常困难的事情&#xff0c;因为PDF文件的格式和内容通常被锁定。为了解决这个问题&#xff0c;出现了PDF修改器这种工具&#xff0c;它可以帮助用户轻松地编辑和修改PDF文件。本文…

RDK X3 Module发布,全新软硬件平台加速实现量产级产品落地

机器人开发是一段美妙的旅程。GEEKROS创始人杨状状是地平线社区的一名开发者&#xff0c;热衷于鼓捣各类机器人&#xff0c;2022年&#xff0c;状状第一时间就拿到了地平线旭日X3派&#xff08;简称旭日X3派&#xff09;&#xff0c;基于TogetheROS™.Bot机器人操作系统&#x…

Win11集成 ChatGPT,任务栏取消分组真的回来了

时隔两月微软如期发布了 Win11 Moments 3 更新&#xff0c;版本号 22621.1778 。 微软这次更新带来了许多质量更新和功能改进。 直观的改动是任务栏&#xff0c;网络图标在连接加密隧道时会上锁&#xff0c;时间显示到秒也重新回归。 日常会用到的 AltTab 任务选项卡被限制到最…

04_Cenos安装Docker

docker安装文档&#xff1a; ubuntu&#xff1a;https://docs.docker.com/engine/install/ubuntu/ centos&#xff1a;https://docs.docker.com/engine/install/centos/ debian&#xff1a;https://docs.docker.com/engine/install/debian/ cenos安装Docker前提&#xff1a; 必…

深入理解Linux虚拟内存管理(二)

系列文章目录 Linux 内核设计与实现 深入理解 Linux 内核&#xff08;一&#xff09; 深入理解 Linux 内核&#xff08;二&#xff09; Linux 设备驱动程序&#xff08;一&#xff09; Linux 设备驱动程序&#xff08;二&#xff09; Linux 设备驱动程序&#xff08;三&#xf…

生态系统NPP及碳源、碳汇模拟、土地利用变化、未来气候变化、空间动态模拟实践技术

由于全球变暖、大气中温室气体浓度逐年增加等问题的出现&#xff0c;“双碳”行动特别是碳中和已经在世界范围形成广泛影响。碳中和可以从碳排放&#xff08;碳源&#xff09;和碳固定&#xff08;碳汇&#xff09;这两个侧面来理解。陆地生态系统在全球碳循环过程中有着重要作…

加盐加密算法

文章目录 为什么需要加密&#xff1f;加盐加密MD5盐值加密Spring Security加盐 为什么需要加密&#xff1f; 从下面的图片中&#xff0c;可以看到用户的密码在数据库中存储时&#xff0c;如果不对密码加密&#xff0c;则是以明文的方式存储的&#xff0c;如果被别人获取到数据…

Mobx和Mobx-react:集中式状态管理

一、Mobx (1) Mobx是一个功能强大&#xff0c;上手非常容易的状态管理工具。 (2) Mobx背后的哲学很简单: 任何源自应用状态的东西都应该自动地获得。只获取与自己相关的数据&#xff0c;不获取无关数据&#xff08;redux则相反&#xff09; (3) Mobx利用getter和setter来收集组…

科技兴警,优云「公安一体化安全运维解决方案」亮相2023警博会

日前&#xff0c;第十一届中国国际警用装备博览会&#xff08;警博会&#xff09;在北京首钢会展中心成功举办&#xff0c;600余家企业参展&#xff0c;集中展示国内外前沿警用装备及尖端技术&#xff0c;大力推进警用装备现代化。 国内领先的平台级数字化运维软件服务商广通优…

TDengine集群搭建

我这里用三台服务器搭建集群 1、如果搭建集群的物理节点上之前安装过TDengine先卸载清空&#xff0c;直接执行以下4条命令 rmtaos rm -rf /var/lib/taos rm -rf /var/log/taos rm -rf /etc/taos2、确保集群中所有主机开放端口 6030-6043/tcp&#xff0c;6060/tcp&#xff0c;…

flink的几种source来源

简单的总结了flink的几种source来源&#xff0c;可以参考下 package com.atguigu.apitestimport java.util.Propertiesimport org.apache.flink.api.common.serialization.SimpleStringSchema import org.apache.flink.streaming.api.functions.source.SourceFunction import …

涨点技巧:注意力机制---Yolov8引入Resnet_CBAM,CBAM升级版

1.计算机视觉中的注意力机制 一般来说,注意力机制通常被分为以下基本四大类: 通道注意力 Channel Attention 空间注意力机制 Spatial Attention 时间注意力机制 Temporal Attention 分支注意力机制 Branch Attention 1.1.CBAM:通道注意力和空间注意力的集成者 轻量级…

版本控制系统有哪些推荐? - 易智编译EaseEditing

以下是几个常用的版本控制系统&#xff08;Version Control System&#xff09;推荐&#xff0c;并对它们进行简单介绍&#xff1a; Git&#xff1a; Git是目前最流行的分布式版本控制系统。它具有高效、灵活和强大的功能&#xff0c;支持快速的代码提交、分支管理、合并操作…

高频面试八股文原理篇(五)索引相关

目录 索引的优缺点 MySQL索引类型 索引原理 常见索引类型 MySQL数据库要⽤B树存储索引⽽不⽤红⿊树、B树、 Hash的原因 怎么验证 MySQL 的索引是否满足需求 聚簇索引和非聚簇索引 索引的优缺点 索引的优点 可以大大加快数据的检索速度&#xff0c;这也是创建索引的最主…

蚂蚁Ant Design组件库的免费在线资源

Ant Design&#xff08;蚂蚁组件&#xff09;是蚂蚁集团体验技术部经过大量项目实践和总结&#xff0c;逐步打磨出的一个设计系统&#xff0c;内含带有 React 的 UI 库。它是为企业级产品设计而创建的。Ant Design 提供了高质量的交互界面设计组件和演示。作为 UI 设计师&#…

Spring:Spring框架结构 ②

一、结构体现的价值 1、可读性强。 2、可维护性。 3、优秀的框架均具有分而治之的思想。清晰的设计、合理的归类、模块化是走向优秀框架的基础性武器。 二、Spring框架的模块划分 1、整体轮廓 Spring框架包含的功能大约由20个小模块组成。这些模块按组可分为核心容器(Core Co…

独立、相关、正交

文章目录 【1. 独立】【2. 相关】【3.正交】【4. 相互关系】相关和独立相关和正交独立和正交独立、不相关和正交小结 【5. 参考文献】 【1. 独立】 独立&#xff1a;对于两个随机变量 y 1 y_1 y1​ 和 y 2 y_2 y2​&#xff0c;若 y 1 y_1 y1​ 的有关信息不给出 y 2 y_2 …

基于AT89C52单片机的无线温度监测设计

点击链接获取Keil源码与Project Backups仿真图&#xff1a; https://download.csdn.net/download/qq_64505944/87848530?spm1001.2014.3001.5503 源码获取 主要内容&#xff1a; 设计一个温度监测器&#xff0c;温度异常时警报器能够响起&#xff0c;设定初始温度&#xff0…

0基础学习VR全景平台篇第33章:场景功能-嵌入标尺

功能位置示意 一、本功能将用在哪里&#xff1f; 嵌入功能可对VR全景作品嵌入【图片】【视频】【文字】【标尺】四种不同类型内容&#xff1b; 本次主要带来标尺类型的介绍&#xff0c;可对VR全景作品中&#xff0c;位置信息较多的场景进行标注&#xff0c;在单场景中植入更多…