【MySQL】深入理解隔离性

深入理解隔离性

  • 一、数据库并发的场景
  • 二、多版本并发控制( MVCC )
  • 三、三个前提知识
    • 1、3个记录隐藏字段
    • 2、undo日志
  • 四、快照的概念
  • 五、Read View
  • 六、隔离级别RR与RC的本质区别

一、数据库并发的场景

数据库并发的场景总共有三种:

  • 读-读:不存在任何问题,也不需要并发控制
  • 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
  • 写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失。
    • 第一类更新丢失也被称为:回滚丢失,是指一个事务的回滚操作导致了另一个已经提交的事务的更新操作丢失。换句话说,当一个事务回滚时,它覆盖了另一个已经提交的事务所做的更改。这可能会导致数据的不一致性和错误。

    • 第二类更新丢失也被称为:覆盖丢失,是指一个事务的提交操作导致了另一个已经提交的事务的更新操作丢失。换句话说,当一个事务提交时,它覆盖了另一个已经提交的事务所做的更改。这也会导致数据的不一致性和错误

说明

  • 读-读并发不需要进行并发控制,写-写并发实际也就是对数据进行加锁,这里最值得讨论的是读-写并发,读-写并发是数据库当中最高频的场景,在解决读-写并发时不仅需要考虑线程安全问题,还需要考虑并发的性能问题。

二、多版本并发控制( MVCC )

多版本并发控制( MVCC )是一种用来解决读-写冲突无锁并发控制

其主要内容是:

  1. 为事务分配单向增长的事务ID,ID越大代表事务越新。
  2. 为每个修改保存一个版本,版本与事务ID关联。
  3. 读操作只读该事务开始前的数据库的快照。

所以MVCC 可以为数据库解决以下问题:

  • 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能。
  • 同时还可以解决脏读,幻读,不可重复读等事务隔离问题。

三、三个前提知识

理解MVCC 需要知道三个前提知识:

  • 3个记录隐藏字段
  • undo 日志
  • Read View (在后面我们进行讲述)

1、3个记录隐藏字段

数据库表中的每条记录都会有如下3个隐藏字段:

  • DB_TRX_ID:6字节,创建或最近一次修改本条记录的事务ID。
  • DB_ROW_ID:6字节,隐含的自增ID(隐藏主键),如果数据表没有主键, InnoDB 会自动以DB_ROW_ID 产生一个聚簇索引。
  • DB_ROLL_PTR:7字节,回滚指针,指向这条记录的上一个版本(这些数据一般在undo log 中)。

此外,数据库表中的每条记录还有一个删除flag隐藏字段,用于表示该条记录是否被更新或删除,所以mysql中的删除并不代表真的删除,只是是删除flag变了,然后当数据需要进行刷盘持久化时,通过查看这个flag字段来进行选择性刷盘。


例如下面的一个学生表,表中包含学生的姓名和年龄。如下:

在这里插入图片描述

当向表中插入一条记录后(假设插入的这条SQL对应的事务ID是9),该记录不仅包含nameage字段,还包含三个隐藏字段。如下:

在这里插入图片描述

  • 因为这是插入的第一条记录,所以隐式主键DB_ROW_ID字段填的就是1。
  • 由于这条记录是新插入的,没有历史版本,所以回滚指针DB_ROLL_PTR的值设置为null
  • MVCC重点需要的就是这三个隐藏字段,实际还有其他隐藏字段,只不过没有画出。

2、undo日志

MySQL的三大日志如下:

  • redo log:重做日志,用于MySQL崩溃后进行数据恢复,保证数据的持久性。
  • bin log:逻辑日志,用于主从数据备份时进行数据同步,保证数据的一致性。
  • undo log:回滚日志,用于对已经执行的操作进行回滚,保证事务的原子性。

MySQL会为上述三大日志在内存中开辟对应的缓冲区,用于存储日志相关的信息,必要时会将缓冲区中的数据刷新到磁盘。

说明

  • MVCC的实现主要依赖三大日志中的undo log,记录的历史版本就是存储在undo log对应的缓冲区中的。

四、快照的概念

现在有一个事务ID为10的事务,要将刚才插入学生表中的记录的学生姓名改为“李四”:

  • 因为是要进行写操作,所以需要先给该记录加行锁。
  • 修改前,先将该行记录拷贝到undo log中,此时undo log中就有了一行副本数据。
  • 然后再将原始记录中的学生姓名改为“李四”,并将该记录的DB_TRX_ID改为10,回滚指针DB_ROLL_PTR设置成undo log中副本数据的地址,从而指向该记录的上一个版本。
  • 最后当事务10提交后释放锁,这时最新的记录就是学生姓名为“李四”的那条记录。

在这里插入图片描述

备注:此时最新的记录是’李四‘那条记录。

现在又有一个事务ID为11的事务,要将刚才学生表中的那条记录的学生年龄改为38:

  • 因为是要进行写操作,所以需要先给该记录(最新的记录)加行锁。
  • 修改前,先将该行记录拷贝到undo log中,此时undo log中就又有了一行副本数据。
  • 然后再将原始记录中的学生年龄改为38,并将该记录的DB_TRX_ID改为11,回滚指针DB_ROLL_PTR设置成刚才拷贝到undo log中的副本数据的地址,从而指向该记录的上一个版本。
  • 最后当事务11提交后释放锁,这时最新的记录就是学生年龄为38的那条记录。

修改后的示意图如下:

在这里插入图片描述

此时我们就有了一个基于链表记录的历史版本链,而undo log中的一个个的历史版本就称为一个个的快照。

说明

  • 所谓的回滚实际就是用undo log中的历史数据覆盖当前数据,而所谓的创建保存点就可以理解成是给某些版本做了标记,让我们可以直接用这些版本数据来覆盖当前数据。
  • 这种技术实际就是基于版本的写时拷贝,当需要进行写操作时先将最新版本拷贝一份到undo log中,然后再进行写操作,和父子进程为了保证独立性而进行的写时拷贝是类似的
  • insert和delete的记录如何维护版本链?
  • 删除记录并不是真的把数据删除了,而是先将该记录拷贝一份放入undo log中,然后将该记录的删除flag隐藏字段设置为1,这样回滚后该记录的删除flag隐藏字段就又变回0了,相当于删除的数据又恢复了。
  • 新插入的记录是没有历史版本的,但是一般为了回滚操作,新插入的记录也需要拷贝一份放入undo log中,只不过被拷贝到undo log中的记录的删除flag隐藏字段被设置为1,这样回滚后就相当于新插入的数据就被删除了。
  • 当前读 VS 快照读

在前面我们讨论了update,delete,insert形成版本链的方式,那么select怎么形成版本链呢?

首先,select不会对数据做任何修改,所以,为select维护多版本,没有意义!

不过,此时对于select有个问题值得被讨论就是:select读取,是读取最新的版本呢?还是读取历史版本?

  • 当前读:读取最新的记录,就叫做当前读。例如使用:select * from 表名 lock in share mode(共享锁)进行当前读。
  • 快照读:读取历史版本,就叫做快照读。一般我们使用的类似于select * from 表名都是快照读,如果没有快照就进行当前读。

所以事务在进行增删查改的时候,并不是都需要进行加锁保护的:

  • 事务对数据进行增删改的时候,操作的都是最新记录,即进行的是当前读,所以需要进行加锁保护。
  • 事务在进行select查询的时候,既可能是当前读也可能是快照读,如果是当前读,那也需要进行加锁保护,但如果是快照读,那就不需要加锁,因为历史版本不会被修改,也就是可以并发执行,这提高了效率,这也就是MVCC的意义所在。

select查询时应该进行当前读还是快照读,则是由隔离级别决定的,在读未提交和串行化隔离级别下,进行的都是当前读,而在读提交和可重复读隔离级别下,既可能进行当前读也可能进行快照读。

  • undo log中的版本链何时才会被清除?
  • undo log中形成的版本链不仅仅是为了进行回滚操作,其他事务在执行过程中也可能读取版本链中的某个版本,也就是快照读。
  • 因此,只有当某条记录的最新版本已经修改并提交,并且此时没有其他事务与该记录的历史版本有关了,这时该记录在undo log中的版本链才可以被清除。

说明

  • 对于新插入的记录来说,没有其他事务会访问它的历史版本,因此新插入的记录在提交后就可以将undo log中的版本链清除了。
  • 因此版本链在undo log中可能会存在很长时间,尤其是有其他事务和这个版本链相关联的时候,但这也没有坏处,这说明它是一个热数据。

五、Read View

  • 事务在进行快照读操作时会生成读视图Read View,在该事务执行快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃的事务ID。

  • Read View在MySQL源码中就是一个类,本质是用来进行可见性判断的,当事务对某个记录执行快照读的时候,对该记录创建一个Read View,根据这个Read View来判断,当前事务能够看到该记录的哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log 里面的某个版本的数据。

ReadView类的源码简化版如下:

class ReadView {// 省略...
private:/** 高水位:大于等于这个ID的事务均不可见*/trx_id_t m_low_limit_id;/** 低水位:小于这个ID的事务均可见 */trx_id_t m_up_limit_id;/** 创建该 Read View 的事务ID*/trx_id_t m_creator_trx_id;/** 创建视图时的活跃事务id列表*/ids_t m_ids;/** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,* 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/trx_id_t m_low_limit_no;/** 标记视图是否被关闭*/bool m_closed;// 省略...
};

部分成员说明:

  • m_ids: 一张列表,记录Read View生成时刻,系统中活跃的事务ID。
  • m_up_limit_id: 记录m_ids列表中事务ID最小的ID。
  • m_low_limit_id: 记录Read View生成时刻,系统尚未分配的下一个事务ID
  • m_creator_trx_id: 记录创建该Read View的事务的事务ID。

我们在实际读取数据版本链的时候,是能读取到每一个版本对应的事务ID的,即:当前记录的DB_TRX_ID ,那么我们现在手里面有的东西就有,当前快照读的ReadView和 版本链中的某一个记录的DB_TRX_ID

所以现在的问题就是,当前快照读应不应该读到当前版本记录,下面的图能够解释这个问题。

在这里插入图片描述

由于事务ID是单向增长的,因此根据Read View中的m_up_limit_idm_low_limit_id,可以将事务ID分为三个部分:

  • 事务ID小于m_up_limit_id的事务,一定是生成Read View时已经提交的事务,因为m_up_limit_id是生成Read View时刻系统中活跃事务ID中的最小ID,因此事务ID比它小的事务在生成Read View时一定已经提交了。
  • 事务ID大于等于m_low_limit_id的事务,一定是生成Read View时还没有启动的事务,因为m_low_limit_id是生成Read View时刻,系统尚未分配的下一个事务ID。
  • 事务ID位于m_up_limit_idm_low_limit_id之间的事务,在生成Read View时可能正处于活跃状态,也可能已经提交了,这时需要通过判断事务ID是否存在于m_ids中来判断该事务是否已经提交。

判断部分

  • 一个事务在进行读操作时,只应该看到自己或已经提交的事务所作的修改,因此我们可以根据Read View来判断当前事务能否看到另一个事务所作的修改。
  • 版本链中的每个版本的记录都有自己的DB_TRX_ID,即创建或最近一次修改该记录的事务ID,因此可以依次遍历版本链中的各个版本,通过Read View来判断当前事务能否看到这个版本,如果不能则继续遍历下一个版本。

源码策略如下(这个函数被调用的在一个循环中,这个循环从最新的历史版本开始向后遍历undo log里面的所有历史版本):

bool changes_visible(trx_id_t id, const table_name_t& name) const MY_ATTRIBUTE((warn_unused_result))
{ut_ad(id > 0);//1、事务id小于m_up_limit_id(已提交)或事务id为创建该Read View的事务的id,则可见if (id < m_up_limit_id || id == m_creator_trx_id) {return(true);}check_trx_id_sanity(id, name);//2、事务id大于等于m_low_limit_id(生成Read View时还没有启动的事务),则不可见if (id >= m_low_limit_id) {return(false);}//3、事务id位于m_up_limit_id和m_low_limit_id之间,并且活跃事务id列表为空(即不在活跃列表中),则可见else if (m_ids.empty()) {return(true);}const ids_t::value_type* p = m_ids.data();//4、事务id位于m_up_limit_id和m_low_limit_id之间,如果在活跃事务id列表中则不可见,如果不在则可见return (!std::binary_search(p, p + m_ids.size(), id));
}

说明: 使用该函数时将版本的DB_TRX_ID传给参数id,该函数是Read View类里面的一个成员函数,其作用就是根据Read View,判断当前事务能否看到这个版本。

六、隔离级别RR与RC的本质区别

我们通过下面的实验现象来理解RR与RC隔离级别

  • 现象演示1

主要操作以及执行顺序:

事务A操作事务A描述事务B描述事务B操作
begin开启事务开启事务begin
--快照读快照读查询select * from account
update account set balance=1789.7 where name=‘张三’;更新 balance=1789.7--
commit提交事务--
--进行快照读select * from account
--进行当前读select * from user lock in share mode

启动两个终端,将隔离级别都设置为可重复读,并查看此时银行用户表中的数据。如下:

在这里插入图片描述

在两个终端各自启动一个事务,在左终端中的事务操作之前,先让右终端中的事务查看一下表中的信息。如下:

在这里插入图片描述

左终端中的事务对表中的信息进行修改并提交,右终端中的事务看不到修改后的数据。如下:

在这里插入图片描述

在右终端中使用select ... lock in share mode命令进行当前读,可以看到表中的数据确实是被修改了,只是右终端中的事务看不到而已。如下:

在这里插入图片描述

  • 现象演示2

主要操作以及执行顺序:

事务A操作事务A描述事务B描述事务B操作
begin开启事务开启事务begin
select * from account快照读快照读select * from account
update account set balance=789.6 where id=7;更新 balance=789.6--
commit提交事务--
--快照读select * from account

如果修改一下SQL的执行顺序,在两个终端各自启动一个事务后,直接先让左终端中的事务对表中的信息进行修改并提交,然后再让右终端中的事务进行查看,这时右终端中的事务就直接看到了修改后的数据。如下:

在这里插入图片描述

这是为什么呢?

  • 因为Read View的形成时机,是在第一次进行快照读的时候形成的

  • 实验1中,右边的终端在左边的修改提交之前进行了快照读,形成了Read View,于是这个Read View认为左边终端中的事务,是和它一起并发运行的事务,它不应该看到左边终端中修改提交后的数据。

  • 实验二中,左边终端的修改没有提交之前,右边的终端在一直没有形成Read View,直到左边的事务结束以后,右边的终端才开始进行快照读,形成Read View,但是这个时候左边的事务已经结束了,于是这个刚形成的Read View就会判定左边终端中的事务是一个已经运行完毕的事务,其内容可以被看到。

  • RR与RC的本质区别
  • RR与RC的本质区别于Read View生成时机!

  • 在RR级别下,事务第一次进行快照读时会创建一个Read View,将当前系统中活跃的事务记录下来,此后再进行快照读时就会直接使用这个Read View进行可见性判断,因此当前事务看不到第一次快照读之后其他事务所作的修改。

  • 而在RC级别下,事务每次进行快照读时都会创建一个Read View,然后根据这个Read View进行可见性判断,因此每次快照读时都能读取到被提交了的最新的数据。

  • RR级别下快照读只会创建一次Read View,所以RR级别是可重复读的,而RC级别下每次快照读都会创建新的Read View,所以RC级别是不可重复读的。

参考文章

MySQL事务管理

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

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

相关文章

JVM中一次完整的GC回收流程

JVM堆内存结构简述 JVM堆内存结构图 堆初体验 所有的对象实例以及数组都要在堆上分配&#xff0c;堆是垃圾收集器管理的主要区域&#xff0c;也被称为“GC 堆”&#xff0c;也是我们优化最多考虑的地方。因为在一个项目中&#xff0c;会不断地创建对象&#xff0c;都是在堆里…

DevOps 教程 (4) - CI/CD 整合

在本第四章的"DevOps 教程"系列中&#xff0c;我们将介绍CI/CD整合的概念和实践。我们会介绍DevOps所带来的好处&#xff0c;包括团队协作、开发效率和产品交付速度的显著提升。 我们还将讨论在DevOps中的不同角色&#xff0c;并理解每个角色在持续集成和持续交付中的…

微调实操一: 增量预训练(Pretraining)

1、前言 《微调入门篇:大模型微调的理论学习》我们对大模型微调理论有了基本了解,这篇结合我们现实中常见的场景,进行大模型微调实操部分的了解和学习,之前我有写过类似的文章《实践篇:大模型微调增量预训练实践(二)》利用的MedicalGPT的源码在colab进行操作, 由于MedicalGPT代…

浅压缩、深压缩、双引擎、计算机屏幕编码……何去何从?

专业视听领域尤其显示控制和坐席控制领域&#xff0c;最近几年最激动人心的技术&#xff0c;莫过于分布式了。 分布式从推出之日就备受关注&#xff1a;担心稳定性的&#xff0c;质疑同步性能的&#xff0c;怀疑画面质量的…… 诚然&#xff0c;我们在此前见多了带着马赛克的…

【C++】类和对象1:类的定义、访问限定符、作用域及对象大小

前言 本文主要是简单的介绍一下类是什么、如何使用 类的定义 class className { // 类体&#xff1a;由成员函数和成员变量组成 };// 一定要注意后面的分号class为定义类的关键字&#xff0c;ClassName为类的名字&#xff0c;{}中为类的主体&#xff0c;注意类定义结束时后面…

智慧文旅:驱动文化与旅游融合发展的新动力

随着科技的快速发展和人们生活水平的提高&#xff0c;文化和旅游的融合成为了时代发展的必然趋势。智慧文旅作为这一趋势的引领者&#xff0c;通过先进的信息技术手段&#xff0c;推动文化与旅游的深度融合&#xff0c;为产业的发展注入新的活力。本文将深入探讨智慧文旅如何成…

【制作100个unity游戏之23】实现类似七日杀、森林一样的生存游戏9(附项目源码)

本节最终效果演示 文章目录 本节最终效果演示系列目录前言回收物品素材绘制UI代码控制垃圾桶回收功能效果 源码完结 系列目录 前言 欢迎来到【制作100个Unity游戏】系列&#xff01;本系列将引导您一步步学习如何使用Unity开发各种类型的游戏。在这第23篇中&#xff0c;我们将…

低成本高效益,电子画册才是品牌的重要选择

​随着互联网的普及和数字化技术的进步&#xff0c;电子画册已成为许多品牌的重要选择。与传统印刷画册相比&#xff0c;电子画册具有低成本、高效益的优点&#xff0c;成为品牌宣传的新趋势。 具体来说&#xff0c;电子画册可以通过在线平台或移动设备轻松查看&#xff0c;无需…

logback自定义生成DB日志(java环境)

目的&#xff1a; 未来在生成日志写入数据库中加一个特殊的字段&#xff0c;官方老版本提供的DBAppender无法实现&#xff0c;并且好巧不巧&#xff0c;在新版本这个实现也被删除了&#xff0c;所以重写一个实现。 1. 安装依赖 安装logback maven依赖 注意&#xff1a; lo…

数据结构——实验01-线性表的链式存储和操作

一、实验内容 二、算法思想与算法实现 1、解题思想 &#xff08;1&#xff09;逆序创建链表La就是使用头插法创建一个链表&#xff0c;所谓头插法就是在创建链表时始终将新元素插入到头结点之后&#xff0c;而正序创建链表Lb就是使用尾插法创建一个链表&#xff0c;所谓尾插法…

[高阶·产品经理]业务建模和需求高阶2月26-3月1日晚8点

等级 高阶 介绍 软件开发中&#xff0c;需求是解决“系统怎样好卖”的问题&#xff0c;设计是解决“降低开发成本”的问题。 本训练聚焦第一个方面&#xff0c;在点上强化业务建模和需求的技能。每期的教材都会根据当期学员所整理的学习《软件方法》的过程中以及工作中碰到的…

conda虚拟环境基础

【一文搞定最新版Anaconda】Win11 安装 Anaconda&#xff08;2023.9&#xff09;详解&#xff08;不删除旧版情况下下载、安装、注册、登录、设置环境变量、迁移旧环境、配置修改换源等&#xff09;连接Pycharm_win11安装anaconda-CSDN博客 conda命令大全&#xff08;create/in…

产品经理必备知识——API接口(获取电商商品订单数据API)

前言 在古代&#xff0c;我们的传输信息的方式有很多&#xff0c;比如写信、飞鸽传书&#xff0c;以及在战争中使用的烽烟&#xff0c;才有了著名的烽火戏诸侯&#xff0c;但这些方式传输信息的效率终究还是无法满足高速发展的社会需要。如今万物互联的时代&#xff0c;我通过…

网络安全之漏洞扫描

漏洞是在硬件、软件、协议的具体实现或系统安全策略上存在的缺陷&#xff0c;从而可以使攻击者能够在未授权的情况下访问或破坏系统。这些缺陷、错误或不合理之处可能被有意或无意地利用&#xff0c;从而对一个组织的资产或运行造成不利影响&#xff0c;如信息系统被攻击或控制…

关于node.js奇数版本不稳定 将11.x.x升级至16.x.x不成功的一系列问题(一)

据说vue2用16稳定一些 vue3用18好一点&#xff08;但之前我vue3用的16.18.1也可以&#xff09; 为维护之前的老项目 先搞定node版本切换 下载nvm node版本管理工具 https://github.com/coreybutler/nvm-windows/releases 用这个nvm-setup.zip安装包 安之前最好先将之前的nod…

基于WordPress开发微信小程序2:决定开发一个wordpress主题

上一篇&#xff1a;基于WordPress开发微信小程序1&#xff1a;搭建Wordpress-CSDN博客 很快发现一个问题&#xff0c;如果使用别人的主题模板&#xff0c;多多少少存在麻烦&#xff0c;所以一咬牙&#xff0c;决定自己开发一个主题模板&#xff0c;并且开源在gitee上&#xff…

计算机网络自顶向下Wireshark labs-HTTP

我直接翻译并在题目下面直接下我的答案了。 1.基本HTTP GET/response交互 我们开始探索HTTP&#xff0c;方法是下载一个非常简单的HTML文件 非常短&#xff0c;并且不包含嵌入的对象。执行以下操作&#xff1a; 启动您的浏览器。启动Wireshark数据包嗅探器&#xff0c;如Wir…

2024年美国大学生数学建模竞赛(E题)财产保险建模|MCDA/随机森林建模解析,小鹿学长带队指引全代码文章与思路

我是鹿鹿学长&#xff0c;就读于上海交通大学&#xff0c;截至目前已经帮500人完成了建模与思路的构建的处理了&#xff5e; 本文运用利用时间序列和强化学习结合DQN算法&#xff0c;解决保险业可持续性问题&#xff1b;采用MCDA和随机森林&#xff0c;应对地产业保险挑战&…

电脑怎么录屏?打造专业级视频内容!

随着科技的进步&#xff0c;电脑已经深入到我们的日常生活和工作中。而在这个数字时代&#xff0c;录制屏幕内容变得日益重要。无论是制作教程、分享游戏技巧&#xff0c;还是记录重要的演示&#xff0c;录屏都是一个不可或缺的功能。可是电脑怎么录屏呢&#xff1f;本文将深入…

Cmake语法学习3:语法

1.双引号 1.1 命令参数 1&#xff09;介绍 命令中多个参数之间使用空格进行分隔&#xff0c;而 cmake 会将双引号引起来的内容作为一个整体&#xff0c;当它当成一个参数&#xff0c;假如你的参数中有空格&#xff08;空格是参数的一部分&#xff09;&#xff0c;那么就可以使…