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

news/2024/10/7 16:06:39/

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/news/1535769.html

相关文章

使用Qt实现实时数据动态绘制的折线图示例

基于Qt的 QChartView 和定时器来动态绘制折线图。它通过动画的方式逐步将数据点添加到图表上&#xff0c;并动态更新坐标轴的范围&#xff0c;提供了一个可以实时更新数据的折线图应用。以下是对代码的详细介绍及其功能解析&#xff1a; 代码概述 该程序使用Qt的 QChartView…

CSS实现服务卡片

CSS实现服务卡片 效果展示 CSS 知识点 回顾整体CSS知识点灵活运用CSS知识点 页面整体布局 <div class"container"><div class"card"><div class"box"><div class"icon"><ion-icon name"color-pal…

【PostgreSQL】提高篇——PostgreSQL 对 JSON 和数组的支持及其在数据建模中的应用

数据的多样性和复杂性日益增加&#xff0c;传统的关系型数据库结构往往难以灵活应对这些变化。PostgreSQL 作为一个强大的开源关系数据库管理系统&#xff0c;提供了对 JSON 和数组数据类型的原生支持&#xff0c;使得开发者能够更灵活地进行数据建模和存储。 一、背景与重要性…

银河麒麟,apt 安装软件报错640Unknown Status

今天把银行麒麟的机器恢复出厂了&#xff0c;然后apt install 安装极其不稳定&#xff0c;故障现象如下图所示&#xff1a; 错误提示里面有&#xff1a; 640 Unknown Status [IP: 106.116.184.122 80] E: 无法下载 http://archive.kylinos.cn/kylin/KYLIN-ALL/pool/universe/f…

OpenHarmony(鸿蒙南向开发)——轻量和小型系统三方库移植指南(一)

往期知识点记录&#xff1a; 鸿蒙&#xff08;HarmonyOS&#xff09;应用层开发&#xff08;北向&#xff09;知识点汇总 鸿蒙&#xff08;OpenHarmony&#xff09;南向开发保姆级知识点汇总~ 持续更新中…… 概述 本文为OpenHarmony开发者提供一些组织编译形式比较常见&…

基于YOLOv8-deepsort算法的智能车辆目标检测车辆跟踪和车辆计数

关于深度实战社区 我们是一个深度学习领域的独立工作室。团队成员有&#xff1a;中科大硕士、纽约大学硕士、浙江大学硕士、华东理工博士等&#xff0c;曾在腾讯、百度、德勤等担任算法工程师/产品经理。全网20多万粉丝&#xff0c;拥有2篇国家级人工智能发明专利。 社区特色…

系统架构设计师教程 第15章 15.3 SOA的参考架构 笔记

15.3 SOA的参考架构 企业集成的架构可划 分为6大类。 (1)业务逻辑服务 (Business Logic Service): 包括用于实现业务逻辑的服务和执行业务 逻辑的能力&#xff0c;其中包括业务应用服务 (Business Application Service)、 业务伙伴服务 (Partner Service) 以及应用和信息资产…

【c语言——指针详解(3)】

文章目录 一、字符指针变量二、数组指针变量1、 数组指针变量是什么&#xff1f;2、 数组指针变量怎么初始化 三、⼆维数组传参的本质四、函数指针变量1、函数指针变量的创建2、函数指针变量的使⽤3、两段有趣的代码1&#xff09;typedef 关键字2&#xff09;typedef和define的…