事务(Transaction)

事务的定义

数据库的事务是一种机制,它包含了一组相关的数据库操作命令,并把这些命令看做一个整体,要么全部执行成功,要么全部失败回滚。

事务的四大特征(ACID)

ACID 是指事务的四个关键特性,分别是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability):

  1. 原子性(Atomicity):事务中的所有操作要么全部执行成功,要么全部回滚,不允许部分执行。
  2. 一致性(Consistency):事务在执行之前和执行之后,数据库都必须保持一致的状态。
  3. 隔离性(Isolation):多个事务同时执行时,每个事务都应该被隔离开来,互不干扰。一个事务的执行不应该受到其他事务的影响,以此来避免并发执行时可能出现的问题,例如脏读、不可重复读和幻读等。
  4. 持久性(Durability):一旦事务提交成功,对数据库的修改就是永久性的,即使系统发生故障或重启,数据也不会丢失。

事务的五种状态

状态描述
活动的(active)事务已经开始执行,但尚未提交,仍在执行中。
部分提交的(partially committed)事务执行了所有的操作和语句,但是提交操作还没有执行完成,只有部分修改已经被写入到数据库中。
失败的(failed)事务执行过程中出现了异常或错误,导致事务无法继续执行下去。
中止的(aborted)事务由于某些原因被强制中止,它可能被用户或系统管理员取消、回滚或者撤销。
提交的(committed)事务执行完成,并且已经将所有的修改操作成功地写入到数据库中,并且提交了事务。

事务的四种隔离级别

MySQL 支持四种隔离级别:读未提交、读已提交、可重复读和串行化,不同的隔离级别提供了不同程度的数据隔离和并发控制能力,默认隔离级别是可重复读(RR)

隔离级别简介可以解决的问题不能解决的问题
读未提交(RU)允许一个事务读取到另一个事务尚未提交的数据脏读、不可重复读、幻读,数据丢失
读已提交(RC)保证一个事务只能读取到已经提交的数据脏读不可重复读,幻读,数据丢失
可重复读(RR)保证一个事务执行期间,多次读取同一数据的结果一致脏读,不可重复读幻读,数据丢失
串行化(S)将所有事务串行执行,确保强一致性脏读,不可重复读,幻读,数据丢失

事务的使用

开启事务(begin)

1
2
3
4
# 方式一
start transaction
# 方式二
begin

START TRANSACTION 语句比 BEGIN 语句更为灵活,因为可以跟随一些修饰符来设置事务的一些特性

修饰符概括
ISOLATION LEVEL 隔离级别设置事务的隔离级别,MySQL支持四种隔离级别
读未提交(READ UNCOMMITTED):可以读取未提交的数据
读已提交(READ COMMITTED):只能读取已提交的数据
可重复读(REPEATABLE READ):多次读取一行数据,保证结果一致
串行化(SERIALIZABLE):将事务串行执行,最高的隔离级别,性能低
READ ONLY标识当前事务是一个只读事务。只能查询,不允许修改任何数据。如果试图修改数据,将会抛出错误
READ WRITE标识当前事务是一个读写事务。可以执行查询和修改操作,这是 MySQL 默认的事务模式

回滚事务(rollback)

如果出错,回滚到任务开始前的状态

1
rollback

提交事务(commit)

提交如果成功,数据会被持久化更改,则数据就被永久的保存在数据库,意味着数据不可以回滚

1
commit

查看事务的默认提交方式(1.自动提交 0.手动提交)

1
select @@autocommit;

修改事务提交方式(1.自动提交 0.手动提交)

1
set @@autocommit=0;

设置事务隔离级别

(1)MySQL 支持四种隔离级别:读未提交(READ UNCOMMITTED)、读已提交(READ COMMITTED)、可重复读(REPEATABLE READ)和串行化(SERIALIZABLE),默认隔离级别是可重复读(REPEATABLE READ)

(2)查看隔离级别

1
2
3
select @@global.tx_isolation;          #查看全局的隔离级别
select @@tx_isolation; #查看会话的隔离级别
show variables like 'tx_isolation'; #查看会话的隔离级别

(3)通过命令设置隔离级别

1
2
set global transaction isolation level 隔离级别;   # 设置全局隔离级别
set session transaction isolation level 隔离级别; # 设置会话隔离级别

(4)通过配置文件(my.cnf 或 my.ini)设置隔离级别

1
transaction-isolation = 隔离级别

(5)启动事务时指定隔离级别

1
START TRANSACTION ISOLATION LEVEL 隔离级别

事务的实现原理

数据库的事务是一种机制,它包含了一组相关的数据库操作命令,并把这些命令看做一个整体,要么全部执行成功,要么全部失败回滚。通过使用事务机制,可以确保数据库操作的原子性、一致性、隔离性和持久性,从而保证了数据的完整性和一致性

原子性的实现

MySQL使用Undo Log(回滚日志)来实现事务的原子性。在事务执行期间,所有的修改操作都会被写入Undo Log(回滚日志)中。如果发生故障或需要回滚事务,MySQL可以利用Undo Log(回滚日志)还原到事务开始之前的状态。

一致性的实现

MySQL通过实现原子性、持久性和隔离性来确保事务的一致性。同时使用Undo Log(回滚日志)和Redo Log(重做日志)记录事务操作。Undo Log(回滚日志)用于回滚操作,Redo Log(重做日志)用于重做操作。MySQL使用Redo Log(重做日志)来保证事务的持久性,即将修改操作刷新到磁盘上的持久存储介质。这样即使系统发生故障,MySQL可以通过Redo Log(重做日志)来恢复已提交的事务,以确保数据的一致性。

隔离性的实现

MySQL通过加锁和多版本并发控制(MVCC)来实现事务的隔离性。

  • 加锁:MySQL使用锁机制来避免并发事务之间的相互干扰。在读取或修改数据时,事务可以获取读锁(共享锁)或写锁(排他锁)。读锁允许其他事务同时持有读锁,但写锁会阻止其他事务获取读锁或写锁,从而确保数据的一致性。
  • MVCC:MySQL通过MVCC机制实现快照读,即每个事务在开始时创建一个一致性读视图,该视图包含在事务开始之前已经提交的数据版本。这意味着事务不会看到其他事务在它开始后进行的修改,从而避免了读事务与写事务之间的冲突。

持久性的实现

MySQL使用Redo Log(重做日志)来实现事务的持久性。当事务提交时,MySQL将修改操作写入Redo Log(重做日志)中,并确保将其刷新到磁盘上的持久存储介质。这样即使系统发生故障,在系统重启后,MySQL可以通过Redo Log(重做日志)来恢复已提交的事务,以确保数据的持久性。

并发事务

什么是并发事务?

并发事务是指多个事务同时执行的情况,当多个用户或应用程序同时访问和操作数据库时,可能会发生并发事务,常见的并发事务的场景如下:

  • 读-读:多个事务同时读取同一个数据,互不干扰,所以不会出现问题
  • 读-写:一个事务读取数据,另一个事务更新数据,可能出现脏读、不可重复读、幻读的问题
  • 写-写:多个事务同时更新同一个数据,可能出现数据丢失问题

并发事务有什么问题?

事务并发可能会出现:脏读(Dirty read)、不可重复读(Non-repeatable read)、幻读(Phantom read)、数据丢失(Lost update)等问题

  • 脏读(Dirty read):一个事务读取到另一个事务尚未提交的数据,如果另一个事务回滚,则读取到的数据就是是无效或错误的。
  • 不可重复读(Non-repeatable read):在同一个事务内,多次读取同一数据,由于其他事务的修改或删除操作,先后得到的结果不一致。
  • 幻读(Phantom read):一个事务在多次查询同一范围内的数据时,由于其他事务的插入或删除操作,结果集中出现了新的行或者消失了原有的行
  • 数据丢失(Lost update):两个或多个事务修改同一条记录,后提交的事务覆盖了先提交的事务的修改结果,导致覆盖丢失的情况。

脏读

  1. 事务A开启事务,并对数据进行修改,修改后还没有提交到数据库中
  2. 在事务A提交修改前,事务B开始执行并访问该数据
  3. 假如事务A发生错误,进行了回滚,那么事务B就读取到事务A修改后未提交的数据,这就是脏读

不可重复读

  1. 事务A开启事务,并读取数据。
  2. 在事务A第一次读取数据后,事务B对这个数据进行了修改并提交到数据库。
  3. 事务A再次读取相同的数据,此时读取到的数据已经发生了变化,和第一次读取时的值不同,这就是不可重复读。

幻读

  1. 事务A开启事务,并执行一个条件查询
  2. 在事务A第一次执行查询后,事务B插入了一条符合查询条件的新数据并提交到数据库。
  3. 事务A再次执行相同的查询,此时查询结果集中出现了新的“幻影行”,这就是幻读。

数据丢失

  1. 事务A开始执行,并修改一条数据。
  2. 事务B开始执行,并基于旧数据执行一些操作。
  3. 事务A提交修改后,数据变为新值。
  4. 事务B提交修改前,由于某些原因(例如锁冲突或死锁),需要回滚到事务开始前状态。
  5. 回滚后,未意识到数据已经被事务A修改过,那么事务B又会把新值替换为旧值。
  6. 此时,数据的新值已经消失了,只留下了旧值。

并发事务的三种解决方案

方式一:使用事务隔离级别

使用事务隔离级别,选择适当的隔离级别来控制事务之间的相互影响。

  • 读未提交:不能解决并发问题,可能导致脏读、不可重复读、幻读、数据丢失等问题。
  • 读已提交:可以解决脏读的问题,但仍可能出现不可重复读、幻读、数据丢失等问题。
  • 可重复读:可以解决脏读和不可重复读的问题,但仍可能出现幻读和数据丢失等问题。
  • 串行化:可以解决脏读、不可重复读、幻读和数据丢失等问题,但会牺牲并发性能。

方式二:使用锁机制

使用锁机制来保护共享资源,确保在一个事务修改共享资源时,其他事务无法同时访问或修改该资源。

  • 共享锁(读锁):允许多个事务同时读取同一数据,但是不能进行写操作。适用于读多写少的情况,可以提高并发性能。
  • 排他锁(写锁):在一个事务进行写操作时,其他事务不能进行任何读写操作。适用于写多读少的情况,保证数据的一致性。

方式三:使用MVCC多版本并发控制

使用MVCC多版本并发控制,为每个事务创建快照,事务读取的是快照而不是实际数据。

  • 读取操作:通过为每个事务创建快照,事务读取的是快照而不是实际数据,避免了并发修改导致的读取问题。
  • 更新操作:更新操作会生成新版本的数据,不会破坏已经存在的数据版本,旧版本的用户仍然可以读取到上一次提交前的数据。

三种方式对比

使用事务隔离级别的优缺点:

  • 优点:可以灵活地处理并发问题,控制读写操作之间的隔离程度
  • 缺点:不同的隔离级别会在一定程度上影响并发性能。

使用锁机制的优缺点:

  • 优点:能够精确控制对数据的访问,保证高并发下的数据一致性和有效性。
  • 缺点:锁的粒度过大或者过小都容易导致死锁和饥饿等问题,加锁操作对于 CPU 的负载也是一个不可忽视的问题。

使用MVCC多版本并发控制的优缺点:

  • 优点:读操作不会被写操作所阻塞,提高了并发性能,处理并发时能够降低锁的粒度,避免锁竞争和死锁等问题
  • 缺点:需要对于数据版本进行管理,因此会对存储空间带来一定的开销。

锁机制(Lock)

锁的概念

MySQL中的锁是一种机制,用于确保在多个并发事务同时访问数据库时数据的完整性和一致性

官网:https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html#innodb-predicate-locks

锁等待(Lock Wait)

锁等待是指当一个事务获取了锁并开始执行时,其他事务需要等待上一个事务释放它的锁,才能占用相应的资源

在MySQL中,innodb_lock_wait_timeout参数用于控制InnoDB存储引擎获取锁的等待超时时间,单位是秒,默认值为50秒。当一个事务等待获取锁的时间超过了该参数指定的值,系统会自动回滚该事务,以避免长时间的锁等待可能导致的性能问题和数据不一致性。可以根据具体的业务需求合理地调整事务的等待超时时间。

  • 较小的超时时间:可以增加锁冲突的风险,因为如果一个事务等待获取锁的时间超过了超时时间,那么它将被回滚并释放锁资源,这样可能会导致其他事务需要等待更长的时间才能获取锁。但是,较小的超时时间能够更快地释放锁资源,适用于高并发场景,可以减少长时间的锁等待,提高系统的并发性能。
  • 较大的超时时间:可以减少锁冲突,因为事务有更长的时间来等待获取锁,这样可以降低其他事务发生锁等待的概率。较大的超时时间适用于对数据一致性要求较高的场景,因为事务有更多的时间来等待其他事务释放锁,这样可以避免数据不一致的可能性。

锁的分类

按照实现方式划分,可以分为乐观锁和悲观锁

  • 乐观锁:认为并发环境下数据不一定会被并发修改,因此只在提交时判断数据是否发生了变化
  • 悲观锁:认为数据在并发环境下总是会被修改,因此需要先加锁再操作数据

按照按颗粒度划分,可以分为全局锁、表级锁、行级锁

  • 全局锁:锁定数整个数据库
  • 表级锁:每次操作锁住整张表
  • 行级锁:每次操作锁住对应的行数据或行间隙

按类型划分,可以分为共享锁(读锁)和排它锁(写锁)

  • 共享锁(读锁):用于保护读操作,多个事务都可以共享同一个锁
  • 排它锁(写锁):用于保护写操作,同一时间只能有一个事务持有锁

锁的语法

查看锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看当前所有事务
select * from information_schema.innodb_trx;
# 查看加锁信息(MySQL5.X)
select * from information_schema.innodb_locks;
# 查看锁等待(MySQL5.X)
select * from information_schema.innodb_lock_waits;
# 查看加锁信息(MySQL8.0
SELECT * FROM performance_schema.data_locks;
# 查看锁等待(MySQL8.0
SELECT * FROM performance_schema.data_lock_waits;
# 查看表锁
show open tables where In_use>0;
# 查看最近一次死锁信息
show engine innodb status;

添加锁

行级锁只能在事务中使用,并且对于同一行的多个锁请求,只有最后提交的请求会生效

在事务中执行 UPDATE、INSERT 和 DELETE 操作时,MySQL 的锁机制会根据需要为相关的数据行添加行级排它锁

1
2
3
4
5
6
7
8
9
10
11
12
# 添加全局锁
flush tables with read lock;

# 为表添加共享锁(表级读锁)
lock tables 表名 read;
# 为表添加排它锁(表级写锁)
lock tables 表名 write;

# 为行添加意向共享锁(行级读锁)
select... lock in share mode;
# 为行添加排它锁(行级写锁)
select... for update;

释放锁

对于全局锁,需要通过命令手动释放

对于表锁,需要通过命令手动释放

对于行锁,由于只能在事务中使用,所以当一个事务提交或者回滚时,会自动释放

1
2
3
4
# 释放所有锁
unlock tables;
# 释放特定表的锁
unlock tables 表名;

乐观锁

乐观锁简介

乐观锁认为并发环境下数据不一定会被并发修改,不需要借助数据库的锁机制,每次去拿数据的时候不会上锁,因此只在提交时判断数据是否发生了变化,如果没被修改过,说明只有自己操作过,正常去修改数据,如果被修改过,则不对数据进行修改,选择放弃、报错重试等策略

乐观锁实现方式

常见实现方式如下:

  1. 版本号机制:在表中添加一个版本号列,每次更新操作都会对版本号进行递增或修改。在更新时,比较当前版本号与最初读取时的版本号是否一致,如果一致则可以执行更新,否则表示有冲突。
  2. 时间戳机制:在表中添加一个时间戳列,记录数据的最后修改时间。在更新时,比较当前时间戳与最初读取时的时间戳是否一致,如果一致就可以执行更新,否则表示有冲突。
  3. CAS(Compare and Swap)机制:某些数据库和编程语言提供了原子操作的支持,如MySQL的SELECT ... FOR UPDATE语句和Java的AtomicReference类,可以使用CAS操作来实现乐观锁。

乐观锁案例

淘宝下单过程中扣减库存的需求(乐观锁实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#(1)首先,需要在商品表中添加一个版本号字段version,用于记录每次对商品信息进行更改的次数
CREATE TABLE `goods` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`name` varchar(50) NOT NULL COMMENT '商品名称',
`stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存',
`version` int(11) NOT NULL DEFAULT '0' COMMENT '版本号',
PRIMARY KEY (`id`),
KEY `idx_version` (`version`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';

#(2)然后,在扣减库存的时候,我们先查询商品信息,并获取当前版本号。
SELECT stock, version FROM goods WHERE id = 1;

#(3)接着,如果库存充足,则更新商品的库存和版本号;否则,直接返回扣减失败的提示信息
BEGIN TRANSACTION; -- 开启事务
SELECT stock, version FROM goods WHERE id = 1; -- 查询商品信息,并获取当前版本号
if(stock >= 数量) { -- 判断库存是否充足
-- 如果库存充足,则更新商品的库存和版本号
UPDATE goods SET stock = stock - 数量, version = version + 1 WHERE id = 1 AND version = 当前版本号;
COMMIT; -- 提交事务
返回扣减成功的提示信息;
} else {
ROLLBACK; -- 回滚事务
返回扣减失败的提示信息;
}

悲观锁

悲观锁简介

认为数据在并发环境下总是会被修改,因此需要先加锁再操作数据,对一个数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,会对该数据进行加锁,拿到锁才能进行操作,否则需要等待释放锁,直到拿到锁才能进行操作

悲观锁实现方式

常见实现方式如下:

  1. 数据库锁机制:通过数据库提供的锁机制实现,如行级锁、表级锁或页级锁等,对需要修改的数据先加锁,其他事务无法修改该数据,直到释放锁。
  2. 应用锁:在应用层代码中使用锁机制来控制并发访问,例如使用Java中的synchronized关键字或分布式锁工具(如Redis的分布式锁)。
  3. 锁表:直接对整个表进行加锁,确保任何事务在操作表时都无法进行并发修改。

悲观锁案例

淘宝下单过程中扣减库存的需求(悲观锁实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#(1)MySql Innodb引擎在SQL中使用悲观锁,首先需要关闭mysql数据库的自动提交属性
set autocommit=0;
#(2)开始事务
begin;
#(3)查询出商品库存信息,并使用selectfor update会把数据给锁住,确保其他线程无法同时修改该行数据
select `quantity` from `items` where `item_id`=123456 for update;
#(4)检查当前库存是否满足用户下单需求,如果库存不足,则回滚事务,释放锁,返回相应提示
if (库存不足) {
rollback;
提示用户库存不足;
}
#(5)如果库存充足,更新库存信息并提交事务
update `items` set `quantity`=`quantity`-1 where `item_id`=123456;
commit;
#(6)如果执行过程中出现任何错误,则回滚事务,并释放锁
catch (Exception $e) {
rollback;
释放锁;
抛出异常或返回错误信息;
}

全局锁(Global lock)

全局锁是对整个 MySQL 数据库实例加锁,添加全局锁后,数据库处于只读状态,增删改操作会处于阻塞状态,防止其他客户端对数据库中任何表进行修改操作,一般用于备份数据库或者调整数据库参数等场景

表级锁(Table-level lock)

表级锁简介

表锁是悲观锁机制中的一种实现方式,用于控制对整个表的并发访问。在表锁中,可以包括表级共享锁、表级排它锁、元数据锁和意向锁等不同类型。

  1. 表级共享锁(Table-level Shared Lock): 表级共享锁允许多个事务同时对表进行读操作,但不允许任何事务进行写操作。也就是说,多个事务可以并发地共享读取表的数据,互不干扰。
  2. 表级排它锁(Table-level Exclusive Lock): 表级排它锁在某个事务对表进行写操作时,会阻塞其他事务对同一表的读写操作。也就是说,在有事务持有表级排它锁的情况下,其他事务无法对该表进行读取或写入操作,直到持有排它锁的事务释放锁。
  3. 元数据锁(Metadata Lock): 元数据锁用于保护数据库对象的元数据(如表结构、索引等)不被修改或删除。当一个事务正在对数据库对象进行修改时,会持有元数据锁,这样其他事务就无法对该对象进行同类的修改操作,以避免冲突。
  4. 意向锁(Intent Lock): 意向锁是表级锁的辅助锁,用于表示事务对表的意向操作。当一个事务请求对表加锁时,会先请求意向锁。意向锁分为意向共享锁和意向排它锁两种,用于协调不同事务对同一表的并发访问。

表级锁特点

表级锁是对某个表加锁,防止其他客户端对该表进行修改操作有以下特点:

  1. 表锁定范围:对整个表进行锁定,意味着其他事务无法对该表进行读取或写入操作。可以确保数据的一致性和完整性,但也限制了其他并发访问的能力。
  2. 锁粒度较大:表级锁是对整个表进行锁定,而不是对单个行或记录进行锁定。这意味着当一个事务持有表级锁时,其他事务无法对表的任何部分进行读取或写入操作,限制了并发访问的能力。
  3. 锁开销较小:相比于行级锁,表级锁的管理开销较小。因为只需要维护少量的锁对象,减少了锁的粒度和数量,从而降低了锁开销。
  4. 锁冲突少:由于表级锁的粒度较大,不同事务之间的冲突较少。只有当两个事务都需要修改同一张表时才会发生冲突。相比于行级锁,表级锁对冲突的潜在可能性较小。
  5. 并发性较低:由于表级锁的粒度较大,当多个事务同时访问同一张表时,会出现阻塞和竞争的情况。当一个事务持有表级排它锁时,其他事务无法读取或修改表中的任何数据,直到排它锁被释放。这降低了并发性能。
  6. InnoDB支持:InnoDB 支持表锁和行锁,MyISAM 仅支持表锁,不支持行锁

表级共享锁(读锁)

(1)共享锁(Shared Locks),又称读锁,简称 S 锁

(2)一个事务获取了一个表或表中数据行的读锁,那么当前事务可以读但是不是增删改,其他事务可以读,不能增删改

  1. 事务A开启事务,获取读锁,可以读取数据
  2. 事务A执行增删改操作,会直接报错
  3. 事务B开启事务,可以读取数据
  4. 事务B执行增删改操作,会进入阻塞状态

表级排它锁(写锁)

(1)排它锁(Exclusive Locks),又称写锁,简称 X 锁

(2)一个事务获取了一个表或表中数据行的写锁,那么当前事务可以读可以增删改,其他事务无法读无法增删改

  1. 事务A开启事务,获取写锁
  2. 事务A可以执行增删改查
  3. 事务B开启事务,执行增删改查,会进入阻塞状态

意向锁(Intent Lock)

意向锁(Intent Lock)是一种由存储引擎自动管理的表级锁,用于提高并发事务操作时的性能和效率,减少死锁风险,分为意向共享锁(IS 锁)和意向排他锁(IX 锁)

  • 意向共享锁(IS 锁):用于表示事务有意向对表中的某些记录加共享锁(S 锁),在加共享锁(S 锁)前必须先取得该表的 IS 锁。
  • 意向排他锁(IX 锁):用于表示事务有意向对表中的某些记录加排他锁(X 锁),在加排他锁(X 锁)前必须先取得该表的 IX 锁。

通过使用意向锁,可以在多个事务并发访问共享资源时,减少死锁的风险和提高性能

  • 如果没有意向锁机制:客户端开启事务并对一张表进行增删改操作时,会自动对涉及到的行添加行级锁。当其他客户端在该表上加锁时,需要逐行检查是否存在行级锁,效率不高。
  • 有了意向锁机制后:客户端一旦开启事务并对表进行增删改操作时,会同时添加行级锁和意向锁。其他客户端尝试在该表上加锁时,会先请求意向锁,来判断是否可以成功加表级锁,并不需要逐行检查行级锁,从而提高并发执行效率。

意向锁只是一种辅助机制,通常由数据库管理系统自动管理,并不需要用户手动干预,当事务提交后,意向共享锁(IS 锁)和意向排他锁(IX 锁)都会自动释放。

锁之间兼容问题

共享锁与排它锁

共享锁(S)排他锁(X)
共享锁(S)兼容互斥
排他锁(X)互斥互斥

意向锁之间互不排斥

意向共享锁(IS)意向排他锁(IX)
意向共享锁(IS)兼容兼容
意向排他锁(IX)兼容兼容

意向锁会与 共享锁 / 排他锁

意向共享锁(IS)意向排他锁(IX)
共享锁(S)兼容互斥
排他锁(X)互斥互斥

元数据锁(Meta Data Lock)

元数据可以简单理解为一张表的表结构,包括表名、列名、数据类型、约束等信息。

元数据锁(MDL)是MySQL 5.5引入的一种锁机制,用于保证表的元数据的一致性。当某张表涉及到未提交的事务时,元数据锁会阻止对该表的元数据进行修改操作。元数据锁是系统自动控制的,不需要显式使用。

  • MDL读锁:当对一个表数据进行增删改查操作的时候,MySQL会自动加MDL读锁;这意味着所有线程都可以正常读取该表的结构和数据,但不能修改表结构,MDL读锁不会影响对表数据的增删改查操作
  • MDL写锁:当对一个表做更新表结构操作的时候,MySQL会自动加MDL写锁;只有拥有MDL写锁的线程才能够对表的元结构进行修改。其他线程在获得MDL写锁之前无法对表结构进行修改操作,并且也无法获取MDL读锁

行级锁(Row-level lock)

行级锁简介

行锁是悲观锁机制中的一种实现方式,用于控制对数据库表中行级数据的并发访问。在行锁中,包括了行级共享锁、行级排它锁、插入意向锁、记录锁、间隙锁和临键锁等不同类型。

  1. 行级共享锁(Row-level Shared Lock): 行级共享锁允许多个事务同时对一行数据进行读操作,但不允许进行写操作。多个事务可以并发地共享读取同一行的数据,互不干扰。
  2. 行级排它锁(Row-level Exclusive Lock): 行级排它锁在某个事务对一行数据进行写操作时,会阻塞其他事务对同一行的读写操作。也就是说,在有事务持有行级排它锁的情况下,其他事务无法对该行进行读取或写入操作,直到持有排它锁的事务释放锁。
  3. 插入意向锁(Insert Intention Lock): 插入意向锁是为了在同一时间内防止多个事务同时往一个页中插入行而引起的问题。当一个事务要往表中插入一行时,会请求插入意向锁。意向锁分为插入意向共享锁和插入意向排它锁,用于协调事务之间的并发插入操作。
  4. 记录锁(Record Lock): 记录锁是对单个记录或行进行的锁定操作,用于保护事务对行的读取或修改操作。当一个事务请求对某行记录加锁时,会获得记录锁,其他事务在此期间无法对该行记录进行写操作。
  5. 间隙锁(Gap Lock): 间隙锁是指在索引上的一个范围(间隙)上设置的锁,用于防止其他事务在这个范围内插入新的记录。可以防止幻读问题的发生。
  6. 临键锁(Next-Key Lock): 临键锁是一种结合记录锁和间隙锁的锁机制,主要用于解决幻读问题。当事务需要读取范围内的数据时,会对范围内的每个记录加上临键锁,确保其他事务无法插入新的记录或修改已有记录。

行级锁特点

行锁也称为记录锁,只针对数据表中的一行数据加锁,是最小粒度的锁,有以下特点:

  1. 行锁定范围:只针对表中的单行或记录进行锁定,其他事务可以并发地访问表中的其他行数据。这提供了更高的并发性能,但也需要更细粒度的锁管理。
  2. 锁粒度较小:行级锁是对表中的单行或记录进行锁定,而不是对整个表进行锁定。这意味着当一个事务持有行级锁时,其他事务仍然可以并发地访问表中的其他行数据,提高了并发性能。
  3. 锁开销较大:相比于表级锁,行级锁的管理开销较大。需要为每一行数据维护锁状态和锁信息,增加了锁的粒度和数量,从而增加了锁管理的开销。
  4. 锁冲突较多:由于行级锁的粒度较小,不同事务之间的冲突较多。当多个事务需要修改同一行数据时,会发生锁冲突,其中一些事务需要等待其他事务释放锁才能继续执行。
  5. 并发性较高:由于行级锁的粒度较小,当多个事务同时访问同一张表时,它们可以同时对不同行进行读取或写入操作,减少了阻塞和竞争的情况,提高了并发性能。
  6. InnoDB支持:InnoDB 支持表锁和行锁,MyISAM 仅支持表锁,不支持行锁

记录锁(Record Lock)

(1)记录锁:锁定一个单行记录

(2)记录锁会对对应的索引记录项加锁,在 读已提交(RC)、可重复读(RR)隔离级别下都支持

(3)记录锁是有 S 锁和 X 锁之分

  • 当一个事务对一条记录加了 S 型记录锁后,其他事务也可以继续对该记录加 S 型记录锁(S 型与 S 锁兼容),但是不可以对该记录加 X 型记录锁(S 型与 X 锁不兼容)
  • 当一个事务对一条记录加了 X 型记录锁后,其他事务既不可以对该记录加 S 型记录锁(S 型与 X 锁不兼容),也不可以对该记录加 X 型记录锁(X 型与 X 锁不兼容)

间隙锁(Gap Lock)

(1)间隙锁:锁定一个范围区间,不包含记录

(2)间隙锁对索引项之间的间隙加锁,加锁之后间隙范围内不允许插入数据

(3)间隙锁只存在于可重复读(RR)隔离级别,防止发生幻读

(4)假如一个索引有1、2、5三个值,那该索引间隙锁的范围为(-∞ , 1),(1 , 3),(3 , 5),(5 , +∞)

(5)假设表中有一个范围 id 为(3,5)间隙锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效的防止幻读现象的发生

临键锁(Next-Key Lock)

(1)临键锁:锁定一个单行记录和范围区间,是记录锁和间隙锁的组合

(2)InnoDB行锁默认加的是临键锁,但可能退化为间隙锁或表锁

(3)临键锁只存在于可重复读(RR)隔离级别,防止发生幻读

(4)假如一个索引有1、2、5三个值,那该索引临键锁的范围为(-∞ , 1],(1 , 3],(3 , 5],(5 , +∞)

(5)假设表中有一个范围 id 为(3,5] 的 临键锁,那么其他事务即不能插入 id = 4 记录,也不能修改 id = 5 的记录

行锁加锁规则

加锁时,使用等值查询命中记录:

  • 如果使用的是聚集索引,则会为该记录加上记录锁;
  • 如果使用的是非聚集索引,MySQL会为该索引的两侧加上间隙锁,同时也会为该记录加上记录锁。
  • 如果没有任何索引,则会升级为表锁。

加锁时,使用等值查询未命中记录:

  • 如果使用的是聚集索引,则会为符合条件的区间加上间隙锁;
  • 如果使用的是非聚集索引,会为该索引的两侧加上间隙锁,并为符合条件的区间加上间隙锁
  • 如果没有任何索引,则会升级为表锁。

加锁时,使用范围查询命中记录:

  • 如果使用的是聚集索引,会为命中的行加上记录锁;
  • 如果使用的是非聚集索引,会为命中的行加上记录锁,同时为该索引的两侧加上间隙锁;
  • 如果没有任何索引,则会升级为表锁。

加锁时,使用范围查询未命中记录:

  • 如果使用的是聚集索引,则会为符合条件的区间加上间隙锁;
  • 如果使用的是非聚集索引,则会为符合条件的区间加上间隙锁;
  • 如果没有任何索引,则会升级为表锁。

行锁加锁案例

(1)准备工作

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE `test` (
`id` int(1) NOT NULL AUTO_INCREMENT,
CREATE TABLE `test` (
`id` int(1) NOT NULL AUTO_INCREMENT COMMENT '聚集索引',
`name` varchar(8) DEFAULT NULL COMMENT '非聚集索引(普通索引)',
`age` int(11) DEFAULT NULL COMMENT '非索引',
PRIMARY KEY (`id`)
) ENGINE=InnoDB;

INSERT INTO `test` (`id`, `name`, `age`) VALUES (3, '测试数据3', 3);
INSERT INTO `test` (`id`, `name`, `age`) VALUES (6, '测试数据6', 6);
INSERT INTO `test` (`id`, `name`, `age`) VALUES (9, '测试数据9', 9);

以上数据,会生成隐藏间隙

1
2
3
4
(-, 3)
(3, 6)
(6, 9)
(9, +∞)

(2)加锁时,使用等值查询命中记录,如果使用的是聚集索引,则会为该记录加上记录锁;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- 事务A开启事务
BEGIN;
SELECT * FROM `test` WHERE `id` = 6 FOR UPDATE;

-- 事务B开启事务
BEGIN;
# 间隙[0,3)
INSERT INTO `test` (`id`, `name`, `age`) VALUES (1, '测试数据1', 1);# 正常执行
INSERT INTO `test` (`id`, `name`, `age`) VALUES (2, '测试数据2', 2);# 正常执行
# 间隙[3,6)
UPDATE test SET id = 30, name = "测试数据30", age = 30 where id = 3;# 正常执行
INSERT INTO `test` (`id`, `name`, `age`) VALUES (4, '测试数据4', 4);# 正常执行
INSERT INTO `test` (`id`, `name`, `age`) VALUES (5, '测试数据5', 5);# 正常执行
# 间隙[6,9)
UPDATE test SET id = 60, name = "测试数据60", age = 60 where id = 6;# 阻塞
INSERT INTO `test` (`id`, `name`, `age`) VALUES (7, '测试数据7', 7);# 正常执行
INSERT INTO `test` (`id`, `name`, `age`) VALUES (8, '测试数据8', 8);# 正常执行
# 间隙[9,+∞)
UPDATE test SET id = 90, name = "测试数据90", age = 90 where id = 9;# 正常执行
INSERT INTO `test` (`id`, `name`, `age`) VALUES (10, '测试数据10', 10);# 正常执行
INSERT INTO `test` (`id`, `name`, `age`) VALUES (11, '测试数据11', 11);# 正常执行

(3)加锁时,使用等值查询命中记录,如果使用的是非聚集索引,会为该索引的两侧加上间隙锁,并为符合条件的区间加上间隙锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- 事务A开启事务
BEGIN;
SELECT * FROM `test` WHERE `name` = "测试数据6" FOR UPDATE;

-- 事务B开启事务
BEGIN;
# 间隙[0,3)
INSERT INTO `test` (`id`, `name`, `age`) VALUES (1, '测试数据1', 1);# 正常执行
INSERT INTO `test` (`id`, `name`, `age`) VALUES (2, '测试数据2', 2);# 正常执行
# 间隙[3,6)
UPDATE test SET id = 30, name = "测试数据30", age = 30 where id = 3;# 阻塞
INSERT INTO `test` (`id`, `name`, `age`) VALUES (4, '测试数据4', 4);# 阻塞
INSERT INTO `test` (`id`, `name`, `age`) VALUES (5, '测试数据5', 5);# 阻塞
# 间隙[6,9)
UPDATE test SET id = 60, name = "测试数据60", age = 60 where id = 6;# 阻塞
INSERT INTO `test` (`id`, `name`, `age`) VALUES (7, '测试数据7', 7);# 阻塞
INSERT INTO `test` (`id`, `name`, `age`) VALUES (8, '测试数据8', 8);# 阻塞
# 间隙[9,+∞)
UPDATE test SET id = 90, name = "测试数据90", age = 90 where id = 9;# 正常执行
INSERT INTO `test` (`id`, `name`, `age`) VALUES (10, '测试数据10', 10);# 正常执行
INSERT INTO `test` (`id`, `name`, `age`) VALUES (11, '测试数据11', 11);# 正常执行

(4)加锁时,使用等值查询命中记录,如果没有任何索引,则会升级为表锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- 事务A开启事务
BEGIN;
SELECT * FROM `test` WHERE `age` = 6 FOR UPDATE;

-- 事务B开启事务
BEGIN;
# 间隙[0,3)
INSERT INTO `test` (`id`, `name`, `age`) VALUES (1, '测试数据1', 1);# 阻塞
INSERT INTO `test` (`id`, `name`, `age`) VALUES (2, '测试数据2', 2);# 阻塞
# 间隙[3,6)
UPDATE test SET id = 30, name = "测试数据30", age = 30 where id = 3;# 阻塞
INSERT INTO `test` (`id`, `name`, `age`) VALUES (4, '测试数据4', 4);# 阻塞
INSERT INTO `test` (`id`, `name`, `age`) VALUES (5, '测试数据5', 5);# 阻塞
# 间隙[6,9)
UPDATE test SET id = 60, name = "测试数据60", age = 60 where id = 6;# 阻塞
INSERT INTO `test` (`id`, `name`, `age`) VALUES (7, '测试数据7', 7);# 阻塞
INSERT INTO `test` (`id`, `name`, `age`) VALUES (8, '测试数据8', 8);# 阻塞
# 间隙[9,+∞)
UPDATE test SET id = 90, name = "测试数据90", age = 90 where id = 9;# 阻塞
INSERT INTO `test` (`id`, `name`, `age`) VALUES (10, '测试数据10', 10);# 阻塞
INSERT INTO `test` (`id`, `name`, `age`) VALUES (11, '测试数据11', 11);# 阻塞

插入意向锁(Insert Intention Locks)

MySQL中插入意向锁(Insert Intention Locks)是一种特殊的锁,用于在进行INSERT操作之前对索引中的空隙加锁。

没有插入意向锁

  1. 如果有两条索引记录分别是4和7,
  2. 事务A想要往其中插入5
  3. 事务B想要往其中插入6
  4. 事务A插入数据时,使用了间隙锁(Gap Lock)或者临键锁(Next-Key Lock)保护
  5. 事务B需要等待事务A释放相应的锁之后才能继续执行插入操作

有了插入意向锁

  1. 如果有两条索引记录分别是4和7,
  2. 事务A想要往其中插入5,会在4和7之间的间隙上设置插入意向锁(Insert Intention Lock)
  3. 事务B想要往其中插入6,会在4和7之间的间隙上设置插入意向锁(Insert Intention Lock)
  4. 由于两个事务都只是在间隙上设置了插入意向锁,它们并不会相互阻塞
  5. 事务A获取到要插入的行的排他锁,并且完成插入操作,会释放插入意向锁
  6. 事务B获取到要插入的行的排他锁,并且完成插入操作,会释放插入意向锁

自增锁(AUTO-INC Locks)

(1)AUTO_INCREMENT是一种用于创建自动增长数字主键列的机制,如果表中的主键列被设置为AUTO_INCREMENT,每当插入一个新记录时,MySQL会自动为该列分配一个比前一个最大值大1的数值

(2)在多用户并发访问的情况下,使用AUTO_INCREMENT可能会导致锁竞争问题。例如,假设同时有两个用户在尝试向一个带有自增主键列的表中插入记录,那么这两个用户可能会在同一时间获得相同的自增值,这将导致主键冲突错误

(3)为了避免这种情况,MySQL引入了自增锁机制,可以保证在同一个事务内插入的记录的AUTO_INCREMENT列的值是连续的。当一个事务进行插入操作时,MySQL会自动获取一个自增锁,任何其他尝试插入带有自增主键的表的操作都必须等待该锁被释放

(4)自增锁只有在原始事务提交或回滚后,才会被释放

行锁实现原理

在 MySQL 中,InnoDB 支持表锁和行锁,MyISAM 仅支持表锁,不支持行锁。

InnoDB 的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁。

如果在数据表中没有适当的索引,MySQL 在实现行锁时会使用基于表的锁来代替基于索引的行级锁。基于表的锁相比基于索引的行级锁需要更多的计算资源和时间。

临键锁是如何防止幻读的发生?

默认情况下,InnoDB工作在可重复读(RR)隔离级别下,并且会以临键锁(Next-Key Lock)的方式对数据行进行加锁,以防止幻读的发生。

  1. 当一个事务执行查询时,在查询过程中,InnoDB会为满足查询条件的索引记录加上行锁,以防止其他事务修改或删除这些记录。
  2. 同时,InnoDB还会对查询的索引记录两边的间隙加上间隙锁,以防止其他事务无法在这个范围内插入新的记录,避免了新插入记录导致幻读的问题。

死锁问题

死锁简介

死锁是指两个或多个事务相互等待对方释放资源而陷入无限等待的状态,从而导致数据库无法操作

死锁案例

  1. 有两个事务 T1 和 T2,分别需要占用 A 和 B 两个资源
  2. T1 先占用资源 A,然后尝试申请资源 B;
  3. T2 先占用资源 B,然后尝试申请资源 A。
  4. T1 等待 T2 释放资源 B,而 T2 等待 T1 释放资源 A,形成了死锁

避免死锁

  1. 尽量减少事务并发访问同一组资源的情况,避免产生死锁。
  2. 在事务中按照固定的顺序获取资源,避免因资源获取顺序不同引起死锁。
  3. 在事务中尽可能缩小锁定的范围,使用行级锁和短事务等方式来减少资源占用时间。
  4. 在数据库设计过程中合理地设置索引,避免全表扫描等操作导致锁定整张表。
  5. 在应用程序中应该及时释放不再需要的锁资源,避免长时间占用资源。

多版本并发控制(MVCC)

当前读(锁定读)

当前读(锁定读)读取的是记录数据的最新版本,显式加锁的都是当前读,如果执行的是下列语句,会获取行级锁,防止其他客户端同时修改相同的数据,都属于当前读(锁定读)

  • select ... lock in share mode:为行添加意向共享锁(行级读锁),其他事务可以读取这些数据,但不能修改它们,直到当前事务释放锁。
  • select ... for update:为行添加排它锁(行级写锁),其他事务无法读取或修改这些数据,直到当前事务释放锁
  • insertupdatedelete :MySQL会在事务中为所涉及的行加上排他锁,其他事务无法同时对被锁定的行进行修改,以避免数据冲突和并发问题。

快照读

快照读读取的是记录数据的可见版本(有旧的版本),普通的select语句都是快照读,不需要加锁,因此可以提高并发性能。MySQL中的InnoDB引擎通过多版本并发控制(MVCC)机制来支持快照读。

MVCC简介

MVCC(Multi-Version Concurrency Control)多版本并发控制,用于实现读写并发性(读取操作和写入操作不会互相阻塞),保证读操作不会被写操作锁定,从而提高系统的并发能力和性能。MVCC对数据做多版本管理,保存数据的历史版本,通过比较数据的版本号来决定数据的是否显示,避免加写锁而导致阻塞,造成写数据时无法读取数据的问题。

MVCC的实现原理

InnoDB通过结合Read View和Undo Log来实现多版本并发控制(MVCC),实现高并发下的数据读取和事务隔离,保证了数据的一致性和并发性。

隐藏字段

在InnoDB存储引擎中,当创建一张表后,每一行数据都会增加额外的隐藏字段,包括DB_ROW_ID、DB_TRX_ID和DB_ROLL_PTR。

  • DB_ROW_ID:隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段。
  • DB_TRX_ID:最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID。
  • DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本。

Undo Log版本链

(1)在事务执行修改操作之前,旧版本的数据会保存到Undo Log中,这个过程通常被称为写入撤销日志。

(2)在事务执行修改操作之后,新的数据版本会保存对应的回滚指针(DB_ROLL_PTR),指向Undo Log中存储的旧数据版本。

(3)Undo Log中存储的旧数据版本,会通过回滚指针连接,形成一个版本链,每个节点代表一个版本,记录了对应行数据的修改信息。

  • 链首:指向最新的数据版本
  • 链尾:指向最旧的数据版本。

(4)当需要回滚一个事务时,MySQL会遍历版本链,按照回滚指针逆向遍历各个节点,并将Undo Log中存储的旧版本数据重新应用到数据库中,从而实现回滚操作。

Read View读视图

在MySQL中,当事务进行快照读(SELECT)的时候,都会生成一个记录快照(Read View),用来做事务的可见性判断

  • 读已提交(RC)隔离级别:事务中每次进行快照读(SELECT)时,都会创建一个新的记录快照(Read View)。这意味着每个快照读都可以看到其他事务已提交的最新数据,但是不能看到其他事务未提交的数据。
  • 可重复读(RR)隔离级别:事务中第一次快照读(SELECT)才创建记录快照(Read View),并在整个事务期间复用这个记录快照(Read View)。保证了事务期间对同一数据的多次快照读结果是一致的,即可重复读。

Read View中有许多字段,用于判断事务对于数据行的可见性,常见的字段如下:

字段简介
trx_id表示当前系统活跃的事务ID(未提交的事务ID)
trx_ids表示当前系统活跃的事务ID列表(未提交的事务ID列表)
up_limit_id表示活跃事务列表中最小的ID(未提交的事务ID列表中的最小ID)
low_limit_id表示出现过的最大的事务 ID+1(尚未分配的下一个事务ID)
creator_trx_id表示创建该Read View的事务ID

事务可见性判断

在执行 SELECT 语句时,InnoDB 会通过数据行的事务 ID(DB_TRX_ID)和 Read View 来判断数据的可见性

  • 如果被访问版本的事务ID = creator_trx_id,表示当前事务访问的是自己修改过的记录,所以该版本可以被当前事务访问。

  • 如果被访问版本的事务ID < up_limit_id,表示生成该版本的事务,在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。

  • 如果被访问版本的事务ID > low_limit_id ,表示生成该版本的事务,在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。

  • 如果low_limit_id >= 被访问版本的事务ID >= up_limit_id ,表示需要判断一下版本的事务ID是不是在 trx_ids 列表中

    如果存在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;
    如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。

MVCC的工作流程

写操作:当一个事务对数据行进行修改时,MySQL会生成一个新的数据行版本,并将新版本的事务ID设置为当前事务ID。旧版本的数据行则通过Undo日志保存,以供其他事务读取旧版本数据。

读操作:当一个事务执行读操作时,首先会获取自己的事务ID并创建一个Read View。然后,事务会根据Read View中的事务ID列表与数据行的版本号进行比较。

  • 如果数据行的版本号小于等于Read View中最小的事务ID,则表示该数据行是可见的,并且事务可以读取这个版本的数据。
  • 如果数据行的版本号大于事务的事务ID,则表示该数据行是不可见的,因为它是由于在事务开始之后创建的。事务需要忽略这个版本,并查找旧版本的数据。
  • 如果数据行的版本号在事务的事务ID范围内但不在Read View的事务ID列表中,则表示该版本已被其他事务修改,并且事务需要查找undo日志以获取旧版本的数据。

MVCC实现原理(RC)

(1)在读已提交(RC)事务隔离级别下,有四个并发事务在执行

事务1事务2事务3事务4
开启事务开启事务开启事务开启事务
插入一条数据
提交事务
修改age为20
查询id为1的数据
提交数据
修改name=李四
查询id为1的数据
提交事务

(2)并发事务形成的版本链如下

(3)在事务4中查询了两次id为1的记录,由于隔离级别为读已提交(Read Committed),所以每次进行快照读都会生成ReadView

第一次进行快照读生成的ReadView如下

1
2
3
4
trx_ids:[2,3,4]             # 未提交的事务ID列表
up_limit_id:2 # 未提交的事务ID列表中的最小ID
low_limit_id:5 # 尚未分配的下一个事务ID
creator_trx_id:4 # 创建该Read View的事务ID

第二次进行快照读生成的ReadView如下

1
2
3
4
trx_ids:[3,4]               # 未提交的事务ID列表
up_limit_id:3 # 未提交的事务ID列表中的最小ID
low_limit_id:5 # 尚未分配的下一个事务ID
creator_trx_id:4 # 创建该Read View的事务ID

(4)ReadView的版本链访问规则:

  • 被访问版本的事务ID = creator_trx_id,可以被当前事务访问

  • 被访问版本的事务ID < up_limit_id,可以被当前事务访问

  • 被访问版本的事务ID > low_limit_id ,不可以被当前事务访问

  • low_limit_id >= 被访问版本的事务ID >= up_limit_id 不在trx_ids列表则可以被当前事务访问

(5)第一次进行快照读在获取数据时,会根据所生成的ReadView以及ReadView的版本链访问规则,到Undo Log版本链中匹配数据:

  • Undo Log版本链中第一条数据的事务ID为3,根据ReadView和ReadView的版本链访问规则可知:①不满足②不满足③不满足④不满足

  • Undo Log版本链中第二条数据的事务ID为2,根据ReadView和ReadView的版本链访问规则可知:①不满足②不满足③不满足④不满足

  • Undo Log版本链中第三条数据的事务ID为1,根据ReadView和ReadView的版本链访问规则可知:①不满足②满足

所以第一次进行快照读在获取数据时,读取到的是事务ID为1的数据

(6)第二次进行快照读在获取数据时,会根据所生成的ReadView以及ReadView的版本链访问规则,到Undo Log版本链中匹配数据:

  • Undo Log版本链中第一条数据的事务ID为3,根据ReadView和ReadView的版本链访问规则可知:①不满足②不满足③不满足④不满足

  • Undo Log版本链中第二条数据的事务ID为2,根据ReadView和ReadView的版本链访问规则可知:①不满足②满足

所以第二次进行快照读在获取数据时,读取到的是事务ID为2的数据

MVCC实现原理(RR)

(1)在可重复读(RR)事务隔离级别下,有四个并发事务在执行

事务1事务2事务3事务4
开启事务开启事务开启事务开启事务
插入一条数据
提交事务
修改age为20
查询id为1的数据
提交数据
修改name=李四
查询id为1的数据
提交事务

(2)并发事务形成的版本链如下

(3)在事务4中查询了两次id为1的记录,由于隔离级别为可重复读(REPEATABLE READ),所以只在第一次快照读时生成ReadView

1
2
3
4
trx_ids:[2,3,4]             # 未提交的事务ID列表
up_limit_id:2 # 未提交的事务ID列表中的最小ID
low_limit_id:5 # 尚未分配的下一个事务ID
creator_trx_id:4 # 创建该Read View的事务ID

(4)ReadView的版本链访问规则:

  • 被访问版本的事务ID = creator_trx_id,可以被当前事务访问

  • 被访问版本的事务ID < up_limit_id,可以被当前事务访问

  • 被访问版本的事务ID > low_limit_id ,不可以被当前事务访问

  • low_limit_id >= 被访问版本的事务ID >= up_limit_id 不在trx_ids列表则可以被当前事务访问

(5)第一次进行快照读在获取数据时,会根据所生成的ReadView以及ReadView的版本链访问规则,到Undo Log版本链中匹配数据:

Undo Log版本链中第一条数据的事务ID为3,根据ReadView和ReadView的版本链访问规则可知:①不满足②不满足③不满足④不满足

Undo Log版本链中第二条数据的事务ID为2,根据ReadView和ReadView的版本链访问规则可知:①不满足②不满足③不满足④不满足

Undo Log版本链中第三条数据的事务ID为1,根据ReadView和ReadView的版本链访问规则可知:①不满足②满足

所以第一次进行快照读在获取数据时,读取到的是事务ID为1的数据

(6)第二次进行快照读在获取数据时,会根据所生成的ReadView以及ReadView的版本链访问规则,到Undo Log版本链中匹配数据:

Undo Log版本链中第一条数据的事务ID为3,根据ReadView和ReadView的版本链访问规则可知:①不满足②不满足③不满足④不满足

Undo Log版本链中第二条数据的事务ID为2,根据ReadView和ReadView的版本链访问规则可知:①不满足②不满足③不满足④不满足

Undo Log版本链中第三条数据的事务ID为1,根据ReadView和ReadView的版本链访问规则可知:①不满足②满足

所以第二次进行快照读在获取数据时,读取到的是事务ID为1的数据