MySQL事务管理

什么是事务?

事务就是一堆DML语句组合在一起的集合,这些语句在逻辑上存在一定的相关性,这一组DML在执行过后只会得到两种结果:1、这一组DML语句全部执行成功过后的结果;2、这一组DML完全没有执行而得到的结果;不会存在执行到一半,然后出错了得到的结果,即使是执行到一半然后出错了,MySQL也会将已经执行的结果恢复到初始状态;因此对于上层用户来说,用户执行一个事务过后就只会得到最终结果和初始结果这两种结果,不会得到处于中间状态的结果;
同时,事务还规定了不同的客户端看到的数据是不一样的!

在一个MySQL服务器内部,可不止我一个客户端在运行,也会有其它客户端与我们一起并发运行,而每个客户端在MySQL内部的代表就是“事务”,也就是说在同一时刻,MySQL内部会同时存在大量的“事务”,那么MySQL服务器内部要不要把这些事务管理起来呢?
答案是要的!如何管理?先描述,再组织!
对于MySQL事务的理解,我们应该将其理解成一个具体的东西,就比如在MySQL服务器内部的一个个事务,在MySQL服务器看来就是一个一个的struct事务结构体或者class事务对象!
当然,正如我们上面所说,一个MySQL数据库,可不止我们一个事务在运行,甚至有大量的请求被包装成事务,在向MySQL服务器发起事务处理请求。而每条事务至少一条SQL语句,当然也有可能会有非常多的SQL语句;如果大家都访问同样的表数据,并且在不加保护的情况下,就绝对会引发线程安全的问题!甚至,因为事务中包含多条SQL,那么要是执行到一半而出错或者不想执行了,那么已经执行的应该怎么办?
所以,一个完整的事务,绝不是简单的SQL,还需要满足下面4个特性:

1. 原子性: 一个事务在被执行过后只会得到最终和初始这两种状态的结果,不会得到任何中间状态的结果!说大白话就是:一个事务在执行过后要么是所有SQL都执行成功过后得到的结果,要么就是一条SQL也没执行得到的结果,不会出现执行一半SQL而得到的结果;因为,当事务执行到一半发生错误过后,MySQL服务会自动撤销该事务之前的所有成功操作,让最后的结果变为初始状态!正是MySQL这个机制,保证了MySQL事务的原子性!
2. 持久性: 事务处理成功过后,对数据的修改是永久的,无法再回滚到初始状态,即便系统出现故障,或者你重启客户端或服务端得到的结果都是修改过后的结果!
3. 隔离性: 数据库允许多个事务并发对数据进行读写、修改的能力,隔离性可以防止多个事务并发执行时由于数据结果互相影响而导致最终数据不一致的问题,根据多个事务之间的影响程度,有了事务隔离级别:读未提交(Read uncommitted)、读提交(Read committed)、可重复读(Repeatable read)、串行化(Serializable);
4. 一致性: 这个特性是由前面3个特性来保证实现的,这个性质是抽象的、MySQL并没有为该性质单独设置机制来保证其实现;在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。说大白话就是,未来事务运行出来的结果是可预知的,如果最后出现了一个不可未在预知之中的结果,那么事务的一致性就被破坏了!

上面四个属性,可以简称为 ACID 。

为什么会出现事务?

事务被设计出来,本质就是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要用户来考虑和维护给种并发执行的问题以及执行失败该怎么办的问题;可以想想一下,当我们使用事务时,要么提交、要么回滚,我们不用去考虑网络异常、服务器宕机了对于当前已经执行的结果的处理,以及事务之间并发访问的问题,因为MySQL内部对于这些异常问题有着自己的一套处理办法,不需要我们上层用户来维护和关心;使用事务,应用程序可以专注于其核心业务逻辑,而无需担心底层的并发和异常处理。这大大简化了应用程序的开发和维护工作。因此,可以说事务是专为应用层服务而设计的,它并非数据库的固有特性,而是在不断的应用实践和数据库优化中逐步发展而来。

事务版本的支持

在MySQL中只有InnoDB存储引擎支持了事务,MyISAM不支持:

show engines;//查看当前MySQL服务支持的存储引擎以及该存储引擎支持那些功能:
在这里插入图片描述

事务提交方式

事务的提交方式有两种:

  1. 手动提交;
  2. 自动提交(MySQL默认配置);
    show variables like 'autocommit';//查看当前会话的提交方式;
    在这里插入图片描述
    set autocommit=0;//关闭当前会话的自动提交;
    在这里插入图片描述
    set autocommit=1;//开启当前会话的自动提交
    在这里插入图片描述
    当我们退出本次会话过后,如果重新登录的化MySQL服务会将本次会话的提交方式重新设置为自动提交:
    在这里插入图片描述

事务常见的操作

创建测试表:
在这里插入图片描述
为了方便后续测试,我们可以先将隔离级别设置为读未提交(Read Uncommitted)
set global transaction isolation level Read Uncommitted;//将全局的隔离级别设置为读未提交(需要重启服务端生效)
select @@global.tx_isoation;//查看全局的隔离级别;
SET SESSION TRANSACTION ISOLATION LEVEL <isolation_level>;//设置当前会话的隔离级别;
SELECT @@SESSION.tx_isolation;;//查看当前会话的隔离级别;

正常演示-证明事务的开始与回滚:
开启自动提交:
在这里插入图片描述
开启一个事务可以start transaction;或者begin;推荐使用begin;
在这里插入图片描述
在MySQL中我们可以使用commit或者rollback来结束掉一个事务;
commit:就是提交本次事务执行的操作,也就是持久化本次事务的操作,并且结束掉本次事务,经过commit的事务无法在进行回滚;
rollback:放弃当前事务已经做的一切操作,让本次事务的执行结果恢复到初始状态,并结束掉本次事务,那种rollback到指定保存点的,不算结束掉一个事务,最后还是需要commit或者rollback来结束掉本次事务;
在这里插入图片描述

非正常演示1 - 证明未commit,客户端崩溃,MySQL自动会回滚(隔离级别设置为读未提交)
自动提交开:

在这里插入图片描述
通过实验结果我们可以看到,当事务A插入一个数据过后,事务B立马就看到了,但是当事务A出现异常退出过后,事务A所做的一切未提交操作都会被MySQL回滚到事务A执行的初始状态,因此我们事务B再次查询数据库的时候发现是空!

非正常演示2 - 证明commit了,客户端崩溃,MySQL数据不会在受影响,已经持久化
在这里插入图片描述
通过实验结果我们可以发现,当一个事务已经提交了它的执行结果之后出现了错误或者异常并不影响已经提交的数据,因为已经提交的数据已经完成了持久化,无论在出现什么异常都不会在回滚已经提交了的结果;

非正常演示3 - 对比试验。证明begin操作会自动更改提交方式,不会受MySQL是否自动提交影响
终端A关闭自动提交;在这里插入图片描述
在这里插入图片描述
终端A开启自动提交:
在这里插入图片描述
在这里插入图片描述
从上面的对比实验来说,autocommit这个开关对于begin启动的事务没有任何关系,begin启动的事务都需要手动commit,无法自动commit;

非正常演示4 - 证明单条 SQL 与事务的关系
终端A开启自动提交模式:
在这里插入图片描述
在这里插入图片描述
通过实验我们可以看到,终端A插入的数据,事务B是可以看到的,即使终端A崩溃了,事务B也依旧能看到终端A插入的数据,这说明终端A插入的数据一定被持久化了;
接着我们来看一看终端A关闭自动提交下的情况:
在这里插入图片描述
在这里插入图片描述
实验现象:终端A插入了一个数据,事务B然后查询能看到终端A插入的数据,紧接着终端A出现异常崩溃,事务B在去查询数据库的时候,终端A插入的数据已经不在了;
实际上通过上面这两个对比实验,我们已经可以得出一些大致结论了,单条SQL实际上也是一个事务,autocommit开关实际上是用来管理单条SQL的;当在开启自动提交的情况下,我们执行的单条SQL会被客户端自动commit,而当我们没有开启自动提交的时候,我们执行的单挑SQL并不会被commit,也就是说这条SQL执行的结果不会被持久化到磁盘,这一点从我们终端A出现异常,终端A插入的数据被回滚可以验证!
我们实际上还可以通过另一个实验来验证我们的单条SQL就是一个事务:
在这里插入图片描述
在自动提交关闭的情况下,我们先使用终端A插入一条数据,然后事务B立马就查询到来这条数据;
紧接着我的终端A手动commit本次SQL执行的结果,然后崩溃掉,紧接着我们事务B再来查询数据库中的结果,发现,上一次终端A插入的结果仍然存在!这也更验证了单条SQL就是一个事务的想法!

总结:

  1. 只要是begin开启的事务,无论autocommit开关是否开启,都必须手动commit才能让数据修改的数据持久化;
  2. 事务可以设置回滚点,当事务执行到一半出现错误过后,MySQL内部会自动进行回滚;
  3. 对于InnoDB存储引擎来说单挑SQL也是一个事务,该事务执行结果是否需要我们进行手动提交,取决于autocommit是否开启;若开启,则不用;若未开启,则需要;
  4. 事务的开始是start transaction 或者 begin 事务的结束可以是commit也可以是rollback;
  5. 如果我们的事务没有设置回滚点,我们用户也可以进行回滚,只不过本次回滚是直接回滚到初始状态;
  6. InnoDB支持事务,MyISAM不支持事务;

事务隔离级别

如何理解事务的隔离性?

我们可以这样理解:
就好比我们在学校考试,基本上都是单人单桌,并且前后左右同学之间都会有一定的间隙或者距离,这是学校用来减少同学们相互交流、相互讨论的一种手段,用来保证每位同学都能不受外界干扰的情况下独立自主的完成自己的卷子;
在MySQL服务内部也是如此,在MySQL服务内部一定会并发运行大量的事务,为了保证这些事务都能互不影响并且独立的完成自己的任务,MySQL也会提供一种“策略”来将这些一个个事务“隔离”开,让一个事务运行的结果不对其它事务的运行造成影响,同理也要保证其它事务之间的运行结果不会对本事务的运行造成影响,从而保证最后数据的一致性和完整性;而根据事务之间的影响程度的不同,就有了隔离级别!

隔离级别

  1. 读未提交【Read Uncommitted】: 在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。(实际生产中不可能使用这种隔离级别的),但是相当于没有任何隔离性,也会有很多并发问题,如脏读,幻读,不可重复读等,我们上面为了做实验方便,用的就是这个隔离性。
    在这里插入图片描述
    通过实验,我们可以发现,事务B能够读取到事务A为commit的数据,我们把这种现象叫做脏读!
    读未提交:就是读取未提交的数据!
    这种隔离级别,几乎没怎么加锁。虽然效率高,但是引发的问题可不止脏读一种,还有不可重复读、幻读等问题,实际开发中几乎不用!
  1. 读提交【Read Committed】 :该隔离级别是大多数数据库的默认的隔离级别(不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。这种隔离
    级别会引起不可重复读,即一个事务执行时,如果多次 select,可能得到不同的结果;
    在这里插入图片描述
    通过实验发现,在读提交隔离级别的情况下,事务B是读取不了事务A所作的修改的,只有当事务A进行了提交,事务B才能读取到事务A的结果;读取提交了的结果,就叫读提交 但是在这种隔离级别下,会出现一个问题,就是事务B重复读取同一张数据库时,得到的结果是不一样的,这种现象叫做不可重复读!
    不可重复读是个问题吗?从我们人的直觉上来说,不就应该读取到别人提交过后的数据吗?
    不可重复读就是个问题:
    eg:
    在这里插入图片描述
    就比如,到年终了,你们的来本准备按照员工工资的等级,来给每个员工发放对应的奖励,于是你的老板就要求你整合出公司每个员工应该得到什么奖品的表格,于是你就开始开启一个事务操作数据库了;
    但是你的同事张三工资是1500元,他觉得这一年来自己功劳不小,于是向老板提出加薪的问题,老板也是十分的豪爽,非常痛快的就答应了,于是就命令另一个程序员小A将数据库中张三的工资调成2500,于是小A呢也开启了一个事务来进行对数据库的修改操作,可是当小A开始查询的时候,你率先查询出了,0~ 1000 、 1000~ 2000的等级的员工名字,而在1000~ 2000这个阶段就有张三的名字,然后紧接着小A这时候又恰好完成了对于张三工资的修改(改为了2500),并且完成了本次修改的提交,于是这时候你在查询2000 ~ 2500元阶段的时候又出现了张三的名字?这不扯吗?一个员工的名字怎么可能出现在两个阶段?这不就是不可重复读带来的问题吗!所以不可重复读也是有问题的!
  1. 可重复读【Repeatable Read】: 这是 MySQL 默认的隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行。但是会有幻读问题。
    在这里插入图片描述
    通过实验,我们可以发现,当事务B去读取事务A未提交的结果时,事务B是查看不到的,同时即使事务A提交了自己的修改结果,事务B依旧查看不了事务A的修改结果,只有等事务B也提交了,并且重新起一个事务才能看到事务A修改的结果,同时我们也发现事务B在自己存活期间重复查询表,得到的都是前后一致的结果,这就叫做可重复读的隔离级别
    但是在这样的隔离界别下仍然会有出现幻读的问题:
    在这里插入图片描述
    像上面这样事务B先查询表,发现表中并没有id=21号的数据,正准备插入,但是与其并发运行的事务A也发现了没有id=21的数据,于是它抢先一步将自己id=21的数据进行了插入,并且插入成功了,而并且完成了自己的提交,这些动作对于事务B来说就是一瞬间完成的,事务B是感觉不到的,紧接着刚才不是说事务B没有查找到id=21的数据吗,于是事务B也进行了自己的插入,但是奇怪的是插入失败了!站在事务B的角度来看这不扯吗?我明明查询出来没有id=21的数据啊,怎么还能插入失败呢?难道是我读取的时候出现幻觉了?像这种情况,我们就叫做幻读!但是对于处于上帝视角的我们来说,我们是很清楚的知道事务B为什么会插入失败的,实际上就是事务B正准备插入的时候,事务A跑的太快,先把位置站了,于是就出现了事务B插入失败的问题!
  1. 串行化【Serializable】: 这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决了幻读的问题。它在每个读的数据行上面加上共享锁。但是可能会导致超时和锁竞争
    (这种隔离级别太极端,实际生产基本不使用);
    在这种隔离级别下,两个事务读取也是需要加锁的,但是两个事务并发读取时是不会被阻塞的;
    eg:事务A先用锁占领了表,然后这时候来了事务B事务B也需要读取表,那么这时候事务B不会被阻塞,mysql允许其并发读取!但是如果这时候事务B想要修改这张表,那么对不起事务B你先阻塞吧,直到我事务A先操作完这张表(也就是事务Acommit)锁才会自动释放,你事务B才有权力操作这张表!
    在这种隔离级别下,很好的保证了最后数据的一致性和完整性,缺点就是串行化程度太严重了,时间消耗太大了!
    在这里插入图片描述

隔离级别如何实现:
隔离,基本都是通过锁实现的,不同的隔离级别,锁的使用是不同的。常见有,表锁,行锁,读锁,写锁,间隙锁(GAP),Next-Key锁(GAP+行锁)等。不过,我们目前现有这个认识就行,先关注上层使用。
在这里插入图片描述
共享锁: 允许其他事务只读取数据,但不能进行修改。因此,当一个事务对数据加上共享锁后,其他事务只能对该数据加读锁,不能做任何修改操作,直到该数据上的读锁被释放。
排他锁: 是不允许其他事务对当前数据进行任何读写操作。当一个事务获取了一个数据行的排他锁后,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务可以对该数据进行读取和修改。

总结:

  1. 隔离界别越严格,安全性越高,但是数据库并发性能也就越低,往往需要在两者之间找到一个平衡点;
  2. MySQL默认隔离界别是可重复读一般不需要修改;
  3. 上面的例子可以看出,事务也有长短事务这样的概念。事务间互相影响,指的是事务在并行执行的时候,即都没有commit的时候,影响会比较大;
    在这里插入图片描述
  4. 实际上在RR、RC级别下,一个事务进行插入,然后读取;然后另一个事务进行读取,很明显,这两个事务读取的不是同一张表,如果是同一张表的话,那么另一个事务在读取的时候一定会读取到插入结果!这实际上是MySQL来实现事务隔离性的一种手段!多版本并发控制( MVCC )

一致性:

  1. 事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库只包含事务成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中
    断,而改未完成的事务对数据库所做的修改已被写入数据库,此时数据库就处于一种不正确(不一
    致)的状态。因此一致性是通过原子性来保证的。
  2. 其实一致性和用户的业务逻辑强相关,一般MySQL提供技术支持,但是一致性还是要用户业务逻辑
    做支撑,也就是,一致性,是由用户决定的。
  3. 而技术上,通过AID保证C

更深层次理解隔离性

数据库并发的场景有三种:
读-读 :不存在任何问题,也不需要并发控制
读-写 :有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
写-写 :有线程安全问题,可能会存在更新丢失问题,这样通常需要加锁来进行完成;
但是在大多数情况下,我们的数据库服务器内部的场景大多都是读-写 并发,为此我们主要讲解读写并发的控制!

读写

多版本并发控制( MVCC ) 是一种用来解决 读-写冲突 的无锁并发控制;
为事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联,读操作只读该事务开始前的数据库的快照。 所以 MVCC 可以为数据库解决以下问题

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

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

  1. 3个记录隐藏字段
  2. undo 日志
  3. Read View

3个记录隐藏列字段

  1. DB_TRX_ID :6 byte,最近修改( 修改/插入 )事务ID,记录创建这条记录/最后一次修改该记录的事
    务ID;
  2. DB_ROLL_PTR: 7 byte,回滚指针,指向这条记录的上一个版本(简单理解成,指向历史版本就行,这些数据一般在 undo log 中)
  3. DB_ROW_ID : 6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键, InnoDB 会自动以DB_ROW_ID 产生一个聚簇索引
  4. 实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了,这样做的话,减少了数据删除过后需要更新索引的成本,是一种以空间换时间的思想;

下面我们用实际样例来说明上述几个隐藏字段:
在这里插入图片描述
由于我们是新插入的数据,为此该数据没有历史版本,因此我们这条数据的回滚指针暂时指向空;
(实际上,对于新插入的数据也是有历史版本与之对应的,该历史版本实际上就是insert语句的反SQL语句也就是delete语句,mysql之所以会记录insert数据的历史版本就是为了方便到时候回滚,这里为了描述简单,我们可以先暂时理解为,新插入的数据没有历史版本!)
当然,如果我们创建的表结构中指名了主键的话,那么mysql是不会生成DB_ROW_ID列的;

undo日志

MySQL 将来是以服务进程的方式,在内存中运行。我们之前所讲的所有机制:索引,事务,隔离性,日志等,都是在内存中完成的,即在 MySQL 内部的相关缓冲区中,保存相关数据,完成各种判断操作。然后在合适的时候,将相关数据刷新到磁盘当中的。

所以,我们这里理解undo log,简单理解成,就是 MySQL 中的一段内存缓冲区,用来保存日志数据的就行。

模拟MVCC

通过模拟MVCC的工作过程,来帮助我们理解隔离性是如何实现的:

假设现在表中已经有一个数据了:
在这里插入图片描述
这时来了一个事务id为10的事务,他要对name=张三的数据改为name=‘李四’;由于要对表做修改操作,该事务在修改前会先对他要修改的行进行加锁;
然后再拷贝一份,它要修改的行数据,将拷贝的数据放入undo日志缓冲区中暂存起来,紧接着事务10,对原始表进行数据修改:将回滚指针进行更新让其指向存在undo缓冲区中的历史数据、更新name=‘李四’、最后更新DB_TRX_ID字段,具体效果如下:
在这里插入图片描述
然后事务10提交,释放锁;
紧接着又来了一个事务11,他要将name=“李四”的年纪改为30,于是它先找到了name=“李四”的这行数据,在修改前,对其进行加锁,然后在修改前,对该行数据进行一份拷贝,同时也会将该拷贝数据暂存与undo缓冲区中,然后在原始数据行更新要修改的数据行的回滚指针、修改name等于李四的数据的年纪、最后更新DB_TRX_ID字段,具体如下:
在这里插入图片描述
然后事务11提交,释放锁
这样,我们就有了一个基于链表记录的历史版本链。所谓的回滚,无非就是用历史数据,覆盖当前数据。上面的一个一个历史版本,我们可以称之为一个一个的快照。

上面是以更新(upadte)主讲的,如果是delete呢?
delete操作与update的操作是一样的:
对于delete的数据,事务在删除之前也会先拷贝一份到undo缓冲区中。这个拷贝的过程实际上是在delete操作前,将需要删除的数据行读取出来并存储到undo缓冲区中。然后,delete操作会将原始数据的flag置为已删除状态(例如,将flag置为true),并且更新DB_TRX_ID字段以记录这个删除操作所属的事务ID。
那如果是insert呢?
insert与delete和update有点不一样,因为insert是插入,也就是之前没有数据,那么insert也就没有历史版本可言,但是一般为了支持回滚操作,mysql也会在undo缓冲区中记录本次insert的相关属性值,比如本次插入的数据的主键值等等,这时原始insert的数据的回滚指针就指向本次记录在undo缓冲区中的insert信息,通过这些insert信息,在未来我们可以很方便的进行回滚!
综上所述, update、delete和insert操作在数据库中形成了不同类型的历史和回滚机制。update和delete操作会形成版本链,而insert操作则通过在undo缓冲区中记录相关属性值来支持事务的回滚操作。
那么select呢?
首先,select不会对数据做任何修改,所以,为select维护多版本,没有意义。不过,此时有个问题,
就是:
select读取,是读取最新的版本呢?还是读取历史版本?
当前读:读取最新的记录,就是当前读。增删改,都叫做当前读select即可能当前读也可能快照读,比如:select lock in share mode(共享锁)(就是当前读),具体select是当前读还是快照读是由不同的隔离级别来决定的;
快照读:读取历史版本(一般而言),就叫做快照读
我们可以看到,在多个事务并发进行增删改的时候,都是当前读,是需要加锁的(注意这个申请锁的过程是以事务为单位来申请的不是以单条SQL),那如果此时又来了一个进行读取操作的事务,如果这个事务也是进行当前读的话,那么也是需要加锁的,这就是串行化;
但如果是快照都,读取历史版本的话,是不受加锁的限制的。也就可以并行执行!换而言之,提高了效率,即MVCC的意义所在;

经过上面的操作我们发现,事务从begin->CURD->commit,是有一个阶段的。也就是事务有执行前,执行中,执行后的阶段。但,不管怎么启动多个事务,总是有先有后的。
那么多个事务在执行中,CURD操作是会交织在一起的。那么,为了保证事务的“有先有后”,是不是应该让不同的事务看到它该看到的内容,这就是所谓的隔离性与隔离级别要解决的问题。
那么,如何保证,不同的事务,看到不同的内容呢?也就是如何如何实现隔离级别?
Read View

Read View

Read View就是事务进行快照读操作的时候生产的 读视图 (Read View)在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大);
Read View 在 MySQL 源码中,就是一个类,本质是用来进行可见性判断的。 即当我们某个事务执行快照读的时候,对该记录创建一个 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;
up_limit_id: 记录m_ids列表中最小的事务ID;
low_limit_id: Read View生成时刻此时mysql内部尚未分配的下一个事务ID(不包括那些回收的事务ID),也就是目前已经分配的事务ID的最大值+1;
creator_trx_id: 创建read view的事务ID;

注意:

  1. m_ids里面的事务ID不一定是连续的,eg:当前一起并发允许的事务id有11、12、13、14、15,在这时候事务13进行了快照读,于是事务13生成了一个read view,可是就在生成的前一秒12、15号事务提前跑完了,那么这时候在生成13号事务的read view的时候就只有11、14号事务正在运行了,于是此时m_ids中就只填11、14;同理此时low_limit_id的值也就是16,因为当前已分配的最大事务ID是15,那么系统尚未分分配的下一个事务id就是16;

我们在实际读取数据版本链的时候,是能读取到每一个版本对应的事务ID的,即:当前记录的
DB_TRX_ID
那么,我们现在手里面有的东西就有,当前快照读的 ReadView 和 版本链中的某一个记录的
DB_TRX_ID 。
所以现在的问题就是,当前快照读,应不应该读到当前版本记录。一张图,解决所有问题!
在这里插入图片描述

如果查到不应该看到当前版本,接下来就是遍历下一个版本,直到符合条件,即可以看到。上面的
readview 是当你进行select的时候,会自动形成

整体流程:

假设当前有条数据:
在这里插入图片描述
事务操作:
在这里插入图片描述
于是根据时间点,事务4会先在undo缓冲区中形成历史版本数据:
在这里插入图片描述
然后接着下来事务4完成了自己的操作,进行了提交,可是此时事务2进行了快照读,于是mysql内部开始为事务2生成了一个read view,在此时此刻,mysql内部正在运行的事务就只剩下,1、3了,于是在2号事务的read view中:
m_ids: 1、3;
up_limit_id :1
low_limit_id : 5
creator_trx_id : 2
在形成好read view过后事务2就去版本链中进行了寻找,拿着表中记录的DB_TRX_ID去跟
up_limit_id,low_limit_id和活跃事务ID列表(trx_list) 进行比较,判断当前事务2能看到该记录的版本。
在这里插入图片描述
结论
故,事务4的更改,应该看到。
所以事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本

RR 与 RC的本质区别

RR隔离级别下测试:

在这里插入图片描述
在这里插入图片描述

RC隔离级别下测试:
在这里插入图片描述

在这里插入图片描述

在上面两个实验中,唯一区别仅仅是 表1 的事务B在事务A修改age前 快照读 过一次age数据
而 表2 的事务B在事务A修改age前没有进行过快照读。

结论:

  1. 事务进行快照读的结果非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读,决定了该事务后续快照读结果的能力;

RR 与 RC的本质区别:

  1. read view的生成时机不同,从而造成RC、RR级别下快照读的结果不同;
  2. 在RR级别下的某个事务如果进行了第一次快照读(也就是执行了select语句),那么此时mysql内部会为这个事务创建一个read view视图对象,这个read view对象会将当前活跃的事务ID存下来,并且在后续的快照读中都是通过第一次形成的read view视图来进行的,因此当两个事务并发执行时(假设这两个事务分别是事务A、事务B),如果事务B在事务A运行期间进行了第一次快照读,那么这时事务B形成的read view对象就会记录下事务A的ID,然后再去版本库中进行读取,然后再读取的时候发现事务A在自己的read view对象中,那么事务B在版本库中读取数据时,并不会读取事务A相关的版本数据,这也就是为什么在RR级别下两个并发运行的事务是看不到彼此对于表数据的修改的;紧接着事务A先运行结束了,那么此时事务B在进行快照读的话,那么还是利用的第一次形成的read view对象来进行快照读的,那么此时事务A的事务ID还是存储在事务B的read view对象中的,因此事务B在继续读取历史版本的时候,还会认为事务A是与自己一起并发运行的事务,因此事务B依旧不会读取事务A相关的历史版本,尽管事务A已经结束并commit了,这也就是为什么在RR级别下,一个事务看不到另一个已经提交了的事务的运行结果!最根本的原因就是事务B所形成的read view太早了!那如果事务A和事务B一起并发运行,但是事务A跑的比较快,先结束了,此时事务B在进行快照读的时候所形成的read view对象中就不会在记录事务A的ID了,因为当前正在活跃的事务没有事务A,因此事务B在后续进行历史版本的读取的时候,是可以读取到事务A的历史版本数据的!
  3. 在RC级别下,也是一样的,与RR级别下不同的是,事务每进行一次快照读,那么该事务都会重新生成一个read view对象,那么每次read view对象记录的当前活跃的事务ID是可能不一样的,因此每次进行快照读的结果也是会不一样的,这也是为什么RC界别下会出现不可重复读的原因!
    举个例子:现有两个事务A、事务B,这两个事务并发运行,在事务A运行期间,事务B进行了多次快照读,那么在RC级别下事务B就会生成多次read view对象,但是这些生成的read view对象中都记录了事务A的ID,因为这些对象是在事务A运行期间形成的,因此每次通过这些read view对象读取数据的时候都不会读取到事务A的相关数据;当事务A提交过后,事务B又进行了一次快照读,那么这时事务B又会重新生成read view对象,那么此时read view对象中并不会记录事务A的ID,因为事务A已经不是当前正在活跃的事务了,于是在后续的读取中事务B会读取事务A相关的历史版本数据,这也是为什么在RC级别下,两个并发的事务在并发期间都读取不到彼此的操作结果,但是当其中一个事务进行了提交,而另一个事务进行了读取,则该事务会看到那个事务提交的结果的原因!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Node.js中使用MySQL服务时,可以通过事务管理来执行多条SQL操作,以确保原子性和数据一致性。事务是将一组操作视为一个单独的单元来执行,要么全部成功,要么全部失败。在Node.js中,可以使用Promise来封装事务操作。 首先,需要导入MySQL的连接池对象,例如使用`const pool = require("../db/mysql")`导入pool对象。然后,可以封装一个执行事务的函数`execTransaction`,该函数接受一个包含多个SQL语句的数组作为参数。 在`execTransaction`函数中,首先通过连接池获取一个数据库连接,然后开始事务。接下来,将所有需要执行的SQL语句封装为Promise数组,每个Promise代表一个SQL执行操作。在Promise中,使用连接对象的`query`方法执行SQL语句,并将结果返回。 使用`Promise.all`来等待所有的SQL操作完成,如果其中有任何一个操作出错,将回滚事务,并且释放连接。如果所有操作都成功执行,将提交事务,并释放连接。最后,通过`resolve`将结果返回。 这样,通过封装的`execTransaction`函数,可以在Node.js中方便地执行MySQL事务管理操作。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [Nodejs实现Mysql事务的解决方案](https://blog.csdn.net/weixin_41464806/article/details/106446449)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [NodeJs使用Mysql模块实现事务处理实例](https://download.csdn.net/download/weixin_38678796/14858646)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南猿北者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值