mysql事务管理

mysql事务管理_第1张图片

文章目录

  • mysql事务管理
      • 事务的概念
      • 为什么会出现事务
      • 事务的版本支持
      • 事务提交的方式
      • 事务操作演示
      • 事务的隔离级别
        • 理解隔离性和隔离级别
        • 隔离级别
        • 查看和设置隔离级别
        • 读未提交 【Read Uncommitted】
        • 读提交 【Read Committed】
        • 可重复读 【Repeatable Read】
        • 串行化 【Serializable】
        • 一致性
      • 多版本并发控制
        • 3个记录隐藏字段
        • undo日志
        • 模拟MVCC
        • Read View
      • RR 和 RC的本质区别

mysql事务管理

事务的概念

事务概念

  • 事务就是一组DML语句组成,这些语句在逻辑上存在相关性,主要用于处理操作量大,复杂度高的数据;例如:转账等多sql语句的业务,包括查询余额(select),减少转账人余额(update),增加指定人的余额(update)…等多条sql语句。 而这些sql语句只要其中一句没有执行成功,转账这个事务就没有完成。

  • 而mysql中,肯定不止一个的事务在运行,同一时刻,可能会有大量的请求被包装成事务,在向mysql服务器发送事务处理请求。而事务中至少包含一条sql语句,肯定会存在大家都在访问同样的表数据,在不加保护的情况下,就绝对会出现并发常见问题(数据不一致…)。也会存在一个事务执行到一半出错或者是不想继续执行的情况;

所以, 一个完整的事务,决定不仅仅是简单的sql集合,还需要满足如下4个属性:

  • 原子性一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
  • **一致性:**在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
  • **隔离性:**数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交( Read uncommitted )、读提交( read committed )、可重复读( repeatable read )和串行化( Serializable )
  • **持久性:**事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

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

原子性(Atomicity,或称不可分割性)

一致性(Consistency)

隔离性(Isolation,又称独立性)

持久性(Durability)。

为什么会出现事务

事务被 MySQL 编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题.可以想一下当我们使用事务时,要么提交,要么回滚,我们不会去考虑网络异常了,服务器宕机了,同时更改一个数据时怎么保证安全;

因此事务本质是为了应用层服务的,而不是伴随着数据库系统天生就有的;

即mysql原本是不需要向我们提供事务这个功能的,但是为了简化客户使用成本,简称就是用得爽一点,不用考虑太多问题;

注: 我们后面把mysql中的一行信息,称为一行记录;


事务的版本支持

show engines;

mysql事务管理_第2张图片

说明:

  • engine : 存储引擎类型
  • support : default 代表当前数据库默认的存储引擎,Yes代表当前mysql支持该存储引擎,No表示不支持
  • comment: 一些相关介绍
  • transactions: 是否支持事务
  • XA: 表示当前存储引擎是否支持XA事务
  • savepoints: 是否支持保存点操作

事务提交的方式

  • 自动提交
  • 手动提交

查看提交方式

show variables like 'autocommit' ;

mysql事务管理_第3张图片

注:ON 代表打开, OFF关闭

设置提交方式的语句

set autocommit = 0/1 ;

例:将提交方式设置为手动提交,即将自动提交关闭

mysql事务管理_第4张图片


事务操作演示

准备工作

我们需要先将msyql的隔离模式设为 读未提交(read uncommitted) ,因为隔离级别越低,越好观察现象;

注:设置完隔离级别之后,需要将服务重启;

set global transaction isolation level read uncommitted;

mysql事务管理_第5张图片

创建测试表

create table if not exists account1(
    id int primary key,
    name varchar(50) not null default '',
    blance decimal(10,2) not null default 0.0
)ENGINE=InnoDB DEFAULT CHARSET=UTF8;	

演示一: 手动回滚操作

为了观察现象方便,我们需要建立俩个连接,连接A用来操作,连接B用来观察现象;

创建测试表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SAgqVplR-1686568388898)(null)]

俩个连接打开事务(start transaction or begin),连接A向表中插入数据

start transaction    ; -- 俩个命令都可以用来打开一个事务
begin;

打开事务,并向account1 插入数据1, 然后创建保存点s1, 然后再插入数据2

mysql事务管理_第6张图片

这时候我们回滚到s1,然后再使用连接B查看account1里面的数据,而如果直接回滚,就会直接回到事务一开始

mysql事务管理_第7张图片

演示二: mysql的原子性

查看当前提交方式为自动提交,而后创建俩个事务,当前表中数据为空

mysql事务管理_第8张图片

连接A向表中插入数据,然后使用连接B查看表中数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vlfdplzx-1686568388957)(null)]

不提交事务,直接将连接A退出,使用连接B查看表中数据

  • 从这里我们可以看出,mysql的事务是保持原子性的,一件事情要么做完了,要么就没做,所以mysql会直接回滚到我们操作之前;

演示三: mysql的持久性

依旧是先创建俩个事务

mysql事务管理_第9张图片

然后使用事务A向表中插入数据,然后使用事务B查看

mysql事务管理_第10张图片

而后先将事务Acommit后然后再退出mysql,而后再使用事务B查看

mysql事务管理_第11张图片

  • 我们会发现即使连接A退出了,但是数据依旧存在,原因是因为mysql的事务具有持久性,事务A提交之后,数据就会持久化到磁盘中;
  • 而且我们发现,当我们打开事务时,提交方式都变成了手动提交,无论我们是否设置自动提交方式

演示四: 验证单条sql语句与事务的关系

我们先在提交方式为自动提交下,进行sql操作,然后终止掉连接A,使用事务B查看插入结果

mysql事务管理_第12张图片

然后我们再将提交方式改为‘手动提交’再重复一遍上面的操作;

mysql事务管理_第13张图片

结果:

我们会发现当我们把提交方式变成手动提交,然后直接终止mysql服务,我们会发现插入操作被回滚了;

mysql事务管理_第14张图片

结论:

  • 只要输入begin或者start transaction,事务便必须要通过commit提交,才会持久化,与是否设置set autocommit无关。

  • 事务可以手动回滚,同时,当操作异常(使用ctrl + D 或ctrl + \信号直接终止掉mysql服务的进程),MySQL会自动回滚

  • 对于 InnoDB 每一条 SQL 语言都默认封装成事务,自动提交。(select有特殊情况,因为 MySQL 有MVCC )

事务操作注意事项:

  • 如果没有设置保存点,也可以回滚,只能回滚到事务的开始。直接使用 rollback(前提是事务还没有提交)

  • 如果一个事务被提交了(commit),则不可以回退(rollback)

  • 可以选择回退到哪个保存点

  • InnoDB 支持事务, MyISAM 不支持事务

  • 开始事务可以使用 start transaction 或者 begin


事务的隔离级别

理解隔离性和隔离级别

  • MySQL服务可能会同时被多个客户端进程(线程)访问,访问的方式以事务方式进行
  • 一个事务可能由多条SQL构成,也就意味着,任何一个事务,都有执行前,执行中,执行后的阶段。而所谓的原子性,其实就是让用户层,要么看到执行前,要么看到执行后。执行中出现问题,可以随时回滚。所以单个事务,对用户表现出来的特性,就是原子性
  • 但,毕竟所有事务都要有个执行过程,那么在多个事务各自执行多个SQL的时候,就还是有可能会出现互相影响的情况。比如:多个事务同时访问同一张表,甚至同一行数据。
  • 数据库中,为了保证事务执行过程中尽量不受其他事务干扰,就有了一个重要特征:隔离性
  • 数据库中,允许事务受不同程度的干扰,就有了一种重要特征:隔离级别

隔离级别

  • 读未提交【Read Uncommitted】: 在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。(实际生产中不可能使用这种隔离级别的),但是相当于没有任何隔离性,也会有很多并发问题,如脏读,幻读,不可重复读等,我们上面为了做实验方便,用的就是这个隔离性。

  • 读提交【Read Committed】 :该隔离级别是大多数数据库的默认的隔离级别(不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读,即一个事务执行时,如果多次 select, 可能得到不同的结果。

  • 可重复读【Repeatable Read】: 这是 MySQL 默认的隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行。但是会有幻读问题。

  • 串行化【Serializable】: 这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决了幻读的问题。它在每个读的数据行上面加上共享锁,。但是可能会导致超时和锁竞争(这种隔离级别太极端,实际生产基本不使用)

注:隔离级别如何实现:隔离,基本都是通过锁实现的,不同的隔离级别,锁的使用是不同的。常见有,表锁,行锁,读锁,写锁,间隙锁(GAP),Next-Key锁(GAP+行锁)等

查看和设置隔离级别

查看隔离级别

select @@global.tx_isolation ; -- 查看全局隔离级别
select @@session.tx_isolation ; -- 查看当前会话全局隔离级别
select @@tx_isolation ; --查看当前隔离级别 默认和当前会话隔离级别相同

mysql事务管理_第15张图片

注:

  • mysql下默认隔离级别都为repeatable read
  • 当前隔离级别通常是和当前会话级别是相同的

设置隔离级别

set global transaction isolation level 隔离级别; -- 设置全局隔离级别
set session transaction isolation level 隔离级别; -- 设置当前会话级别

设置全局隔离级别

mysql事务管理_第16张图片

设置会话隔离级别

mysql事务管理_第17张图片

注:

  • 设置全局隔离级别,只会影响往后的所有的连接,所以设置了全局隔离级别,需要重启服务;
  • 设置当前会话级别,只会影响本次连接,不会影响其他连接,不需要重启服务;

读未提交 【Read Uncommitted】

设置隔离级别为read uncommitted

mysql事务管理_第18张图片

说明:使用俩个连接 , 连接A进入插入数据,连接B查看数据

mysql事务管理_第19张图片

使用连接A更新表中数据,我们发现连接A并未将事务commit,而连接B却能看到A事务所做出的修改

mysql事务管理_第20张图片

  • 其中,我们将一个事务能看到另一个事务还没提交的数据的现象叫做“脏读” ,对应的数据就称为脏数据

读提交 【Read Committed】

设置隔离级别未read committed,而后重启服务

mysql事务管理_第21张图片

然后使用连接A更新数据,使用连接B查看数据;

mysql事务管理_第22张图片

我们会发现连接A在未将事务提交时,连接B确实是看不到数据的,而在连接A将事务提交之后,是能看到连接A更新的数据的;

注:

  • 一个事务先后俩次select 的所看到的结果不同的现象,就叫做不可重复读

可重复读 【Repeatable Read】

设置全局隔离级别未repeatable read ,重启服务,并查看一下当前数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jtLsVm5p-1686568388985)(null)]

mysql事务管理_第23张图片

而后使用连接A向表中更新数据和插入数据,使用B在连接Acommit前和commit后查看数据

mysql事务管理_第24张图片

我们会发现无论是连接commit前和commit后,连接B都是查看不到事务A更新的数据,也就是说repeatable read 隔离级别下是解决了‘脏读’和‘不可重复读’这俩个问题的;

但是,一般的数据库在可重复读情况的时候,无法屏蔽其他事务insert的数据(为什么?因为隔离性实现是对数据加锁完成的,而insert待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题),会造成虽然大部分内容是可重复读的,但是insert的数据在可重复读情况被读取出来,导致多次查找时,会多查找出来新的记录,就如同产生了幻觉。这种现象,叫做幻读(phantom read)

验证:mysql中是否具有幻读问题

依旧是先查看表中的数据,然后使用俩个连接分别启动俩个事务;

mysql事务管理_第25张图片

而后我们使用连接A向表中插入一条新数据,使用连接B观察现象

mysql事务管理_第26张图片

在事务B提交之后,就可以看到事务A更新的数据了

mysql事务管理_第27张图片

很明显,MySQL在RR级别的时候,是解决了幻读问题的(解决的方式是用Next-Key锁(GAP+行锁)解决的


串行化 【Serializable】

先将全局隔离级别置成Serializable,然后重启服务

mysql事务管理_第28张图片

查看目前的表中数据,确保隔离级别为串行化

mysql事务管理_第29张图片

查看表中数据,我们会发现查看数据是不会发生串行化

mysql事务管理_第30张图片

但是进行插入操作时,我们发现如果事务B没有commit,事务A的插入操作是一直被堵塞的,而且如果在等待时间里事务B还未commit,事务A的插入操作就会超时失败,即事务A就会失败

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AWap2K7g-1686568388927)(null)]

总结:

  • 其中隔离级别越严格,安全性越高,但数据库的并发性能也就越低,往往需要在两者之间找一个平衡点。
  • 不可重复读的重点是修改和删除:同样的条件, 你读取过的数据,再次读取出来发现值不一样了
  • 幻读的重点在于新增 :同样的条件, 第1次和第2次读出来的记录数不一样; 即幻读现象主要是针对于mysql的insert操作的,
  • 说明: mysql 默认的隔离级别是可重复读,一般情况下不要修改
隔离级别 脏读 不可重复读 幻读 是否加锁读
读未提交(read uncommitted) Yes Yes Yes 不加锁
读已提交(read committed) No Yes Yes 不加锁
可重复读(repeatable read) No No No 不加锁
可串行化 (serializable) No No No 加锁

Yes:会发生该问题 No:不会发生该问题

注:一般的数据库repeatable隔离模式下,都会存在幻读现象,但从上面验证来看mysql中是不存在幻读现象的


一致性

  • 事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库只包含事务成功提交的结果时,数据库处于一致性状态。

  • 如果系统运行发生中断,某个事务尚未完成而被迫中断,而改未完成的事务对数据库所做的修改已被写入数据库,此时数据库就处于一种不正确(不一致)的状态。因此一致性是通过原子性来保证的。

  • 如果数据库因为出现某种问题,导致数据库中的某些数据丢失了,而客户又以为这些数据依旧存在,此时数据库就会处于一种不正确(不一致)的状态。因此一致性是通过持久性来保证的。 --例子: 某位购买了车票的乘客,因购买记录在数据库中不慎丢失,导致乘客登车时,查找不到购买记录,但是乘客有扣费记录;就会导致很严重的问题

  • 例如多个事务同时操作数据库中的数据,如果不对这些事务加以隔离,就会造成多个事务并行执行时,不同事务看到的数据可能会出现‘脏读’,‘不可重复读’,‘幻读’等现象,会让数据库处于一种不一致的状态。因此一致性是通过隔离性来保证的;

  • 而技术上,通过AID保证C的;

  • 而且要保证事务的一致性,不仅需要AID这三个属性,还需要程序员的sql语句是符合当前事务的一致性逻辑的。-- 例子: 某程序员在银行工作,所写的转账事务,只写了减少转账人的余额的sql,而增加被转账人的余额的sql却没有;

所以说保障事务的一致性,不仅需要原子性,隔离性,持久性来保证,还得需要程序员自身的代码逻辑是没有问题的;即一致性是由mysql和用户一同保证的


多版本并发控制

数据并发的场景有以下3种:

  • 读-读 :不存在任何问题,也不需要并发控制

  • 读-写 :有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读

  • 写-写 :有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失

说明:

  • 写-写并发场景下的第一类更新丢失又叫做回滚丢失,即一个事务的回滚把另一个已经提交的事务更新的数据覆盖了,第二类更新丢失又叫做覆盖丢失,即一个事务的提交把另一个已经提交的事务更新的数据覆盖了。
  • 读-读并发不需要进行并发控制,写-写并发实际也就是对数据进行加锁,这里最值得讨论的是读-写并发,读-写并发是数据库当中最高频的场景,在解决读-写并发时不仅需要考虑线程安全问题,还需要考虑并发的性能问题。

多版本并发控制(MVCC)

  • 多版本并发控制(Multi-Version Concurrency Control )是一种用来解决 读-写冲突 的无锁并发控制

  • 为事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联,读操作只读该事务开始前的数据库的快照。

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

3个记录隐藏字段

  • DB_TRX_ID :6 byte,最近修改( 修改/插入 )事务ID,记录创建这条记录/最后一次修改该记录的事务ID

  • DB_TRX_ID : 7 byte,回滚指针,指向这条记录的上一个版本(简单理解成,指向历史版本就行,这些数据一般在 undo log 中)

  • DB_ROW_ID : 6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键, InnoDB 会自动以 DB_ROW_ID 产生一

    个聚簇索引

  • 补充:实际上还有一个删除flag字段,即记录被更新或者删除并不代表真的删除,而是将flag置1;

例:

创建一个测试表并插入一条数据

create table if not exists student(
    name varchar(11) not null,
    age int not null
);

insert into student (name,age) values ('张三',28);

mysql事务管理_第31张图片

而实际上该表结构是这样的:

name age DB_TRX_ID(创建该记录的事务ID) DB_ROW_ID(隐式主键) DB_ROLL_PTR(回滚指针)
张三 28 null 1 null

说明:

  • 我们目前不知道创建该记录的事务ID,隐式主键,我们就默认设为null,1。
  • 这条记录是新插入的,没有之前版本,所以回滚指针指向null
  • MVCC重点需要的就是这3个隐藏字段,实际上还有其他字段,只是没有画出来

undo日志

MySQL的三大日志如下:

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

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

说明:

  • MVCC的实现主要依赖undo log
  • MySQL 将来是以服务进程的方式,在内存中运行。索引,事务,隔离性,日志等机制,都是在内存中完成的,即在 MySQL 内部的相关缓冲区中,保存相关数据,完成各种判断操作。然后在合适的时候,将相关数据刷新到磁盘当中的。
  • 所以可以将undo log就简单理解成mysql中的一段内存缓冲区,用来保存历史版本数据的;

详细介绍日志可参考这篇文章


模拟MVCC

模拟MVCC

  • 现在有一个事务10(仅仅为了好区分),对student表中记录进行修改(update):将name(张三)改成name(李四)。事务10,因为要修改,所以要先给该记录加行锁。
  • 修改前,现将改行记录拷贝到undo log中,所以,undo log中就有了一行副本数据。(原理就是写时拷贝)
  • 所以现在 MySQL 中有两行同样的记录。现在修改原始记录中的name,改成 ‘李四’。并且修改原始记录的隐藏字段 DB_TRX_ID 为当前 事务10 的ID, 我们默认从 10 开始,之后递增。而原始记录的回滚指针 DB_ROLL_PTR 列,
  • 里面写入undo log中副本数据的地址,从而指向副本记录,既表示我的上一个版本就是它。
  • 事务10提交,释放锁

mysql事务管理_第32张图片

现在又有一个事务11,对student表中记录进行修改(update):将age(28)改成age(38)

  • 事务11,因为也要修改,所以要先给该记录加行锁。
  • 修改前,现将改行记录拷贝到undo log中,所以,undo log中就又有了一行副本数据。此时,新的副本,我们采用头插方式,插入undo log。
  • 现在修改原始记录中的age,改成 38。并且修改原始记录的隐藏字段 DB_TRX_ID 为当前 事务11 的ID。而原始记录的回滚指针 DB_ROLL_PTR 列,里面写入undo log中副本数据的地址,从而指向副本记录,既表示我的上一个版本就是它。
  • 事务11提交,释放锁。

mysql事务管理_第33张图片

  • 从上面我们可以看出,不同事务操作同一个数据,所看到的数据版本可能是不一样的
  • 上面的不同版本实际上就是一个链表结构,也叫历史版本链 ,而所谓的回滚,实际上就是使用回滚指针指向的数据版本,覆盖掉当前数据

Read View

快照概念

上面的一个一个版本数据,就称为一个一个快照

select , insert ,delete 是否会具有历史版本链呢?

  • delete操作并不是真的将数据删除,而是将flag字段置为1,代表该字段要删除,所以deleteupdate是一样的,会具有历史版本链
  • insert :因为insert是插入,也就是之前没有数据,那么insert也就没有历史版本。但是一般为了回滚操作,insert的数据也是要被放入undo log中(实际上就是将当前数据拷贝一份,然后将flag置1,代表删除,放到undo log),如果当前事务commit了,那么这个undo log 的历史insert记录就可以被清空了
  • 首先,select不会对数据做任何修改,所以,为select维护多版本,没有意义;但我们现在上面的数据是有很多不多版本的,而我们select的时候读那个版本的数据呢? 读取不同版本就分为了快照读和单纯读

当前读和快照读

  • 当前读:读取的是最新的记录,就是当前读。增删改,都叫做当前读
  • 快照读: 读取历史版本,就叫做快照读;

其中,insert , update ,delete ,select lock in share mode (共享锁),select for update 都是当前读

即只有select操作具有快照读和当前读,其他操作都是当前读

Read View

  • Read View就是事务进行 快照读 操作的时候生产的 读视图 (Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)

  • Read View 在 MySQL 源码中,就是一个类(因为mysql是用c++写的),本质是用来进行可见性判断的。 即当我们某个事务执行快照读的时候,对该记录创建一个 Read View 读视图,把它比作条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的 undo log 里面的某个版本的数据。

下面是部分源码:

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最小的ID(没有写错)
low_limit_id; //ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1(也没有写错)
creator_trx_id //创建该ReadView的事务ID

说明:

  • 可以将low_limit_id 理解为下一个事务创建时所分配的事务ID;
  • m_ids : 可以理解为同一时期内创建的事务ID;同一时期:[up_limit_id , low_limit_id -1 ]
  • up_limit_id : 当前活跃组中事务ID的最小值;

如何判断当前快照读,是否能读取到某个版本的数据呢?看下图

  • 我们在实际读取数据版本链的时候,是能读取到每一个版本对应的事务ID的,即:当前记录的 DB_TRX_ID

  • 那么,我们现在手里面有的东西就有,当前快照读的 ReadView 和 版本链中的某一个记录的 DB_TRX_ID

注:图中id指定是 DB_TRX_ID 即当前版本数据的最近修改或创建ID

mysql事务管理_第34张图片

对应的源码策略:

说明:

  • 事务ID是由小到大分配的,即事务ID越大,表示当前事务创建的时间越晚;所以当前事务ID能读取到的创建或修改记录版本的事务,一定是比当前事务早提交的;
  • 而判断事务提交早晚的不能仅仅通过ID大小,还需要再做其他判断;

mysql事务管理_第35张图片

说明:

  • 如果版本记录的DB_TRX_ID == 当前的creator_trx_id ,表示的是该记录可能是刚刚由本事务修改的,所以当前事务是可以读取到该版本记录的;
  • DB_TRX_ID > low_limit_id 即表示当前记录版本的ID,是比当前所有活跃的事务都要晚创建的,而我们快照读时只能读历史版本,且该ID不属于同一时期事务的ID,所以当前事务不能读取当前版本的记录
  • 当前记录的ID与当前事务是同一时期的事务时,还需要判断创造该记录版本的事务是否在当前事务创建读视图之前提交

整体流程

假设当前有条记录:

name age DB_TRX_ID(创建该记录的事务ID) DB_ROW_ID(隐式主键) DB_ROLL_PTR(回滚指针)
张三 28 null 1 null

事务操作:

事务1 [id = 1] 事务2 [id = 2] 事务3 [id = 3] 事务4 [id = 4]
begin begin begin begin
修改且已提交
进行中 快照读 进行中
  • 事务4操作: 修改name(张三) 变成name(李四)
  • 当事务2对某行记录执行了快照读时,数据库为该行记录生成了一个Read View读视图

事务2的 Read View 读视图

m_ids; // 1,3
up_limit_id; // 1
low_limit_id; // 4 + 1 = 5,原因:ReadView生成时刻,系统尚未分配的下一个事务ID
creator_trx_id // 2

即当前Read View 认为同一时期的事务是[1,4]

此时数据的版本链是这样的

mysql事务管理_第36张图片

  • 只有事务4修改过记录,并在事务2执行快照读前,就提交了事务

mysql事务管理_第37张图片

  • 我们的事务2在快照读该行记录的时候,就会拿该行记录的 DB_TRX_ID 去跟 up_limit_id,low_limit_id和活跃事务ID列表(trx_list) 进行比较,判断当前事务2能看到该记录的版本。

判断过程:

//事务2的 Read View
m_ids; // 1,3
up_limit_id; // 1
low_limit_id; // 4 + 1 = 5,原因:ReadView生成时刻,系统尚未分配的下一个事务ID
creator_trx_id // 2
    
//事务4提交的记录对应的事务ID
DB_TRX_ID=4
    
//比较步骤
DB_TRX_ID(4)< up_limit_id(1) ? 不小于,下一步
DB_TRX_ID(4)>= low_limit_id(5) ? 不大于,下一步
m_ids.contains(DB_TRX_ID) ? 不包含,说明,事务4不在当前的活跃事务中。
    
//结论
故,事务4的更改,应该看到。
所以事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本

结论:

  • 当前事务所能看到的记录版本,创建该条记录的事务ID不一定比当前事务ID要小
  • 事务进行快照读某条行记录时,会先拿该行记录的DB_TRX_ID 去跟up_limit_id ,low_limit_id 活跃事务ID列表(trx_list) 进行比较,判断当前事务能看到该记录的版本,如果不满足,就通过当前记录的回滚指针找到历史版本,重复上述操作,直到找到匹配的记录版本;

RR 和 RC的本质区别

当前读和快照读在RR级别下的区别

--设置RR模式下测试
mysql> set global transaction isolation level REPEATABLE READ;
Query OK, 0 rows affected (0.00 sec)

--重启终端

mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)

--依旧用之前的表
create table if not exists account(
    id int primary key,
    name varchar(50) not null default '',
    blance decimal(10,2) not null default 0.0
)ENGINE=InnoDB DEFAULT CHARSET=UTF8;
--插入一条记录,用来测试
mysql> insert into user (id, age, name) values (1, 15,'黄蓉');
Query OK, 1 row affected (0.00 sec)

测试用例1-表1:

事务A操作 事务A描述 事务B描述 事务B操作
begin 开启事务 开启事务 begin
select * from user 快照读(无影响查询) 快照读查询 select * from user
update user set age = 18 where id = 1 更新age = 18 - -
commit 提交事务 - -
select 快照读,没有读到age = 18 select * from user
select lock in share mode 当前读,读到age = 18 select * from user lock in share mode

测试用例ku2-表2:

事务A操作 事务A描述 事务B描述 事务B操作
begin 开启事务 开启事务 begin
select * from user 快照读,查到age = 18
uddate user set age = 28 where id =1; 更新age = 28
commit 提交事务
select 快照读age =28 select * from user
select lock in share mode 当前读 age = 28 select * from user lock in share mode
  • 用例1与用例2:唯一区别仅仅是 表1 的事务B在事务A修改age前 快照读 过一次age数据

  • 而表2的事务B在事务A修改age前没有进行过快照读。

结论:

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

    注: 上速操作虽然是update, 但delete操作数据同样如此

RR 和RC的本质区别

RR : Repeatable Read RC: Read Committed

  • 正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同
  • 在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务记录起来
  • 此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过
  • 快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改记录版本不可见;
  • 即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见
  • 而在RC级别下的,事务中,每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因
  • 总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。
  • 正是RC每次快照读,都会形成Read View,即RC每次快照读几乎都可以认为是当前读,所以,RC才会有不可重复读问题。

总结:

  • MVCC模式主要是针对 RR 和 RC隔离级别,读未提交是对任何操作都不加锁,串行化是对任何操作都进行加锁;而RR 和 RC 使用锁 + MVCC 实现的效率和安全的兼容
  • MVCC多版本并发模式的设计优点就是: 写当前,读历史 ;实现了写操作使用锁保证安全性,并行读操作不需要进行上锁,而是通过读历史不同的数据,解决了不可重复读问题,使得不同的事务可以高效率的安全的并行访问数据

其中解决了幻读问题的方案是next- key锁(gap + 行锁);

感兴趣的可参考下面几篇博客:

照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改记录版本不可见;

  • 即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见
  • 而在RC级别下的,事务中,每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因
  • 总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。
  • 正是RC每次快照读,都会形成Read View,即RC每次快照读几乎都可以认为是当前读,所以,RC才会有不可重复读问题。

总结:

  • MVCC模式主要是针对 RR 和 RC隔离级别,读未提交是对任何操作都不加锁,串行化是对任何操作都进行加锁;而RR 和 RC 使用锁 + MVCC 实现的效率和安全的兼容
  • MVCC多版本并发模式的设计优点就是: 写当前,读历史 ;实现了写操作使用锁保证安全性,并行读操作不需要进行上锁,而是通过读历史不同的数据,解决了不可重复读问题,使得不同的事务可以高效率的安全的并行访问数据

其中解决了幻读问题的方案是next- key锁(gap + 行锁);

感兴趣的可参考下面几篇博客:
博客1

博客2

你可能感兴趣的:(mysql,mysql,数据库)