第 21 章 一条记录的多幅面孔——事务的隔离级别与 MVCC

embedded/2024/10/15 10:32:49/

21.1 事前准备

CREATE TABLE hero ( number INT, NAME VARCHAR ( 100 ), country VARCHAR ( 100 ), PRIMARY KEY ( number ) 
) ENGINE = INNODB CHARSET = utf8;INSERT INTO hero VALUES ( 1, '刘备', '蜀' );

21.2 事务隔离级别

在保证事务隔离性的前提下,使用不同的隔离级别,来尽量提高多个事务访问同一数据的性能。

21.2.1 事务并发执行遇到的问题
  1. 脏写(Dirty Write):一个事务修改了另一个未提交事务修改过的数据。
  2. 脏读(Dirty Read):一个事务读到了另一个未提交事务修改过的数据。
  3. 不可重复读(Non-Repeatable Read):一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交,该事务都能查询得到最新值。
  4. 幻读(Phantom):一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次查询时,能把另一个事务插入的记录也读出来。
21.2.2 SQL 标准中的四种隔离级别

按严重性:

脏写 > 脏读 > 不可重复读 > 幻读

隔离级别中文名脏读不可重复读幻读
READ UNCOMMITTED读未提交PossiblePossiblePossible
READ COMMITTED读已提交Not PossiblePossiblePossible
REPAEATABLE READ可重复读Not PossibleNot PossiblePossible
SERIALIZABLE串行化Not PossibleNot PossibleNot Possible

不论哪种隔离级别,都不允许脏写的情况发生。

21.2.3 MySQL 中支持的四种隔离级别

MySQL 的默认隔离级别为 REPEATABLE READ ,我们可以手动修改一下事务的隔离级别。

# 如何设置事务的隔离级别
# SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;#  查看当前会话默认的隔离级别
SHOW VARIABLES LIKE 'transaction_isolation';

21.3 MVCC 原理

21.3.1 版本链

对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列:

  1. trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给 trx_id 隐藏列。
  2. roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo log 中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

在这里插入图片描述

Tips:实际上 insert undo 只在事务回滚时起作用,当事务提交后,该类型的 undo log 就没用了。

假设之后有两个事务 id 分别为100和200的事务对这条记录进行 UPDATE 操作:

在这里插入图片描述

对该记录每次更新后,都会将旧值放到一条 undo log 中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,我们把这个链表称为 版本链,它的头节点就是当前记录最新的值,另外,每个版本还包含生成该版本时对应的事务 id。

在这里插入图片描述

21.3.2 ReadView
  1. 对于 READ UNCOMMITTED 级别的事务来说,直接读取记录的最新版本;
  2. 对于 SERIALIZABLE 级别的事务来说,使用加锁的方式来访问;
  3. 对于 READ COMMITED 和 REPAEATABLE READ 级别的事务来说,必须保证讲到已经提交了的事务修改过的记录,而不能直接读取最新版本版本的记录。那么怎么判断版本链中哪个版本是当前事务可见的?于是便有了 ReadView 的概念。

ReadView 主要包括以下4个重点:

  1. m_ids:表示在生成 ReadView 时当前系统中活跃的读写事务的**事务 id **列表
  2. min_trx_id:表示在生成 ReadView 时当前系统中活跃的读写事务中最小的**事务 id **,也就是 m_ids 中的最小值。
  3. max_trx_id:表示生成 ReadView 时系统中应该分配给下一个事务的 id 值。
  4. creator_trx_id:表示生成该 ReadView 的事务的 事务 id

TIPS:只有在对表中的记录做改动时才会为事务分配事务 id,否则在一个只读事务中的事务 id 都默认为 0。

通过 ReadView 就可以判断访问某条记录时,某个版本是否可见:

  1. 如果被访问版本的 trx_id 与 ReadView 中的 creator_trx_id 相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
  2. 如果被访问版本的 trx_id 小于 ReadView 中的 min_trx_id,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。
  3. 如果被访问版本的 trx_id 大于 ReadView 中的 max_trx_id,表明生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。
  4. 如果被访问版本的 trx_id 属性值在 ReadView 的 min_trx_id 和 max_trx_id 之间,那就需要判断一下 trx_id 是不是在 m_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。
public boolean isShow(long trx_id, long creator_trx_id, long min_trx_id, long max_trx_id, List<Long> m_ids) {if (trx_id == creator_trx_id) {return true;}if (trx_id < min_trx_id) {return true;}if (trx_id > max_trx_id) {return false;}return !m_ids.contains(trx_id);}

如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本,直到最后一个版本。

READ COMMITTED 和 REPEATABLE READ 生成 ReadView 的时机不同。

21.3.2.1 READ COMMITTED——每次读取数据前都生成一个ReadView
# Transaction 100
BEGIN;
UPDATE hero SET name = '关羽' WHERE number = 1;
UPDATE hero SET name = '张飞' WHERE number = 1;# Transaction 200
BEGIN;# 更新了一些别的表的记录
...

此时 number=1 的记录版本链:

在这里插入图片描述

使用 READ COMMITED 隔离级别的事务开始执行查询:

# 使用READ COMMITTED隔离级别的事务
BEGIN;# SELECT1:Transaction 100、200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'

SELECT1 的执行过程如下:

  1. 在执行 SELECT 语句时会先生成一个 ReadView:

    {m_ids:[100, 200],min_trx_id: 100,max_trx_id: 201,creator_trx_id: 0
    }
    
  2. 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 name = ‘张飞’ ,trx_id = 100 ,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。

  3. 下一个版本的列 name = ‘关羽’ ,trx_id = 100 ,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。

  4. 下一个版本的列 name = ‘刘备’ ,trx_id = 80 ,小于 ReadView 中的 min_trx_id 值100 ,所以这个版本是符合要求的,最后返回给用户的版本就是这条列 name 为 ‘刘备’ 的记录。

之后,把事务id=100的事务提交,并使用事务id=200的事务继续更新

# Transaction 100
BEGIN;
UPDATE hero SET name = '关羽' WHERE number = 1;
UPDATE hero SET name = '张飞' WHERE number = 1;
COMMIT;# Transaction 200
BEGIN;# 更新了一些别的表的记录
...UPDATE hero SET name = '赵云' WHERE number = 1;
UPDATE hero SET name = '诸葛亮' WHERE number = 1;

此时 number=1 的记录版本链:

在这里插入图片描述

再次使用 READ COMMITED 隔离级别的事务开始执行查询:

# 使用READ COMMITTED隔离级别的事务
BEGIN;# SELECT1:Transaction 100、200均未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'# SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'张飞

SELECT2 的执行过程如下:

  1. 在执行 SELECT 语句时会先生成一个 ReadView:

    {m_ids:[200],min_trx_id: 200,max_trx_id: 201,creator_trx_id: 0
    }
    
  2. 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 name = ‘诸葛亮’ ,trx_id = 200 ,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。

  3. 下一个版本的列 name = ‘赵云’ ,trx_id = 200 ,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。

  4. 下一个版本的列 name = ‘张飞’ ,trx_id = 100 ,小于 ReadView 中的 min_trx_id 值200 ,所以这个版本是符合要求的,最后返回给用户的版本就是这条列 name 为 ‘张飞’ 的记录

每次 SELECT 查询都可以读到最新已提交的记录,这就是读已提交

21.3.2.2 REPAEATABLE READ——在第一次读取数据时生成一个ReadView
# Transaction 100
BEGIN;
UPDATE hero SET name = '关羽' WHERE number = 1;
UPDATE hero SET name = '张飞' WHERE number = 1;# Transaction 200
BEGIN;# 更新了一些别的表的记录
...

此时 number=1 的记录版本链:

在这里插入图片描述

使用 REPAEATABLE READ 隔离级别的事务开始执行查询:

# 使用REPEATABLE READ隔离级别的事务
BEGIN;# SELECT1:Transaction 100、200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备

SELECT1 的执行过程如下:

  1. 在执行 SELECT 语句时会先生成一个 ReadView:

    {m_ids:[100, 200],min_trx_id: 100,max_trx_id: 201,creator_trx_id: 0
    }
    
  2. 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 name = ‘张飞’ ,trx_id = 100 ,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。

  3. 下一个版本的列 name = ‘关羽’ ,该版本的 trx_id = 100 ,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。

  4. 下一个版本的列 name = ‘刘备’ ,该版本的 trx_id = 80 ,小于 ReadView 中的 min_trx_id 值100 ,所以这个版本是符合要求的,最后返回给用户的版本就是这条列 name 为 ‘刘备’ 的记录。

之后,把事务id=100的事务提交,并使用事务id=200的事务继续更新

# Transaction 100
BEGIN;
UPDATE hero SET name = '关羽' WHERE number = 1;
UPDATE hero SET name = '张飞' WHERE number = 1;
COMMIT;# Transaction 200
BEGIN;# 更新了一些别的表的记录
...UPDATE hero SET name = '赵云' WHERE number = 1;
UPDATE hero SET name = '诸葛亮' WHERE number = 1;

此时 number=1 的记录版本链:

在这里插入图片描述

再次使用 REPAEATABLE READ 隔离级别的事务开始执行查询:

# 使用 REPEATABLE READ 隔离级别的事务
BEGIN;# SELECT1:Transaction 100、200均未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'# SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值仍为'刘备

SELECT2 的执行过程如下:

  1. 使用 REPEATABLE READ 隔离级别时,因为之前执行 SELECT1 时已经生成过 ReadView 了,所以此时不再重新生成,而是直接复用之前的 ReadView

    {m_ids:[100, 200],min_trx_id: 100,max_trx_id: 201,creator_trx_id: 0
    }
    
  2. 然后从版本链中挑选可见的记录,从图中可以看出,最新版本的列 name = ‘诸葛亮’ ,trx_id = 200 ,在 m_ids 列表内,所以不符合可见性要求,根据 roll_pointer 跳到下一个版本。

  3. 下一个版本 name = ‘赵云’ ,trx_id = 200 ,也在 m_ids 列表内,所以也不符合要求,继续跳到下一个版本。

  4. 下一个版本的列 name = ‘张飞’ ,trx_id = 100 ,也在 m_ids 列表内,所以也不符合要求,同理下一个列 name 的内容是 ‘关羽’ 的版本也不符合要求。继续跳到下一个版本。

  5. 下一个版本的列 name = ‘刘备’ ,trx_id = 80 ,小于 ReadView 中的 min_trx_id 值
    100 ,所以这个版本是符合要求的,最后返回给用户的版本就是这条列 c 为 ‘刘备’ 的记录。

两次 SELECT 查询得到的结果都是相同的,这就是所谓的可重复读

21.3.3 二级索引与MVCC

前面使用的 trx_id 和 roll_pointer 隐藏列都只存在于聚簇索引,当使用二级索引查询时,该如何判断可见性?

BEGIN;SELECT name FROM hero WHERE name = '刘备';
  1. 二级索引页面的 Page Header 部分有一个名为 PAGE_MAX_TRX_ID 的属性,每次有事务增删改本页面中的记录时,如果该事务的 id 大于 PAGE_MAX_TRX_ID ,就把这个 id 赋值给 PAGE_MAX_TRX_ID。这也就意味 PAGE_MAX_TRX_ID 是修改该二级索引页面的最大事务 id。
  2. 当 SELECT 语句访问某个二级索引记录时,如果对应的 ReadView 的 min_trx_id 大于该页面的 PAGE_MAX_TRX_ID,说明该页面的所有记录都对该 ReadView 可见,否则执行步骤 3。
  3. 利用二级索引进行回表,得到对应的聚簇索引记录后按照前面说的方式判断是否可见。
21.3.4 MVCC 小结
  1. 所谓 MVCC(Multi-Version Concurrency Control,多版本并发控制)指的就是在使用 READ COMMITED、REPEATABLE READ 这两种隔离级别的事务在执行普通的 SELECT 操作时访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。
  2. READ COMMITED、REPEATABLE READ 这两种隔离级别主要不同在于生成 ReadView 的时机。
  3. 只有在进行普通 SELECT 查询时,MVCC 才生效。

21.4 关于 purge

在合适的时候把 update undo 日志以及仅仅被标记为删除的记录彻底删除掉,这个删除操作就称为 purge。

21.5 总结

  1. 并发的事务在运行过程中会出现一些可能的引发一致性问题
  2. SQL 标准中有4种隔离级别
  3. 版本链
  4. ReadView

http://www.ppmy.cn/embedded/123959.html

相关文章

软考面向对象 ----(封装,继承,多态,面向对象设计原则)

文章目录 一、面向过程和面向对象二、类三、对象&#xff0c;消息四、重载五、封装六、继承&#xff08;extends&#xff09;七、多态八、静态&#xff0c;动态绑定九、面向对象设计原则十、面向对象分析&#xff0c;设计&#xff0c;测试&#xff0c; 一、面向过程和面向对象 …

C++类和对象下详细指南

C类和对象下详细指南 1. 初始化列表与构造函数 1.1 初始化列表概述 初始化列表在C中用于初始化对象的成员变量&#xff0c;特别是当你需要在对象构造时就明确成员变量的值时。通过初始化列表&#xff0c;成员变量的初始化可以在进入构造函数体之前完成。这不仅可以提升性能&…

SpringMVC源码-AbstractHandlerMethodMapping处理器映射器将@Controller修饰类方法存储到处理器映射器

SpringMVC九大内置组件之HandlerMapping处理器映射器-AbstractHandlerMethodMapping类以及子类RequestMappingHandlerMapping如何将Controller修饰的注解类以及类下被注解RequestMapping修饰的方法存储到处理器映射器中。 从RequestMappingHandlerMapping寻找: AbstractHandle…

从DBA是“擦车的”谈起

前段时间有网友在群里讨论&#xff0c;把数据库代码工作者比做是造车的&#xff0c;业务应用开发人员是开车的&#xff0c;而数据库管理员(DBA)则是擦车的。有网友评论这句话&#xff0c;“伤害性不大&#xff0c;侮辱性极强”。说实在的&#xff0c;个人觉得这个说法虽然有些偏…

Java LeetCode刷题

3254. 长度为 K 的子数组的能量值 I package JavaExercise20241006;import java.util.ArrayList; import java.util.Collections;public class JavaExercise {public static void main(String[] args) {int[] nums {1,2,3,4,3,2,5};Solution solution new Solution();int[] …

Linux中环境变量

基本概念 环境变量Environmental variables一般是指在操作系统中用来指定操作系统运行环境一些参数。 我们在编写C、C代码时候&#xff0c;在链接的时候从来不知道我们所链接的动态、静态库在哪里。但是还是照样可以链接成功。生成可执行程序。原因就是相关环境变量帮助编译器…

精准识别IP类型:有效判断住宅IP与机房IP的方法

对于很多网络业务来说&#xff0c;IP地址的类型多种多样&#xff0c;其中住宅IP和机房IP&#xff08;数据中心IP&#xff09;是两种常见的分类。了解如何分辨这两种IP地址&#xff0c;对于注册营销、运营直播、广告投放等领域都有重要意义。下面探讨住宅IP和机房IP的特点&#…

初识算法 · 滑动窗口(1)

目录 前言&#xff1a; 长度最小的子数组 题目解析 算法原理 算法编写 无重复长度的最小字符串 题目解析 算法原理 算法编写 前言&#xff1a; 本文开始&#xff0c;介绍的是滑动窗口算法类型的题目&#xff0c;滑动窗口本质上其实也是双指针&#xff0c;但是呢&#…