MySQL - 第11节 - MySQL事务管理
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
目录
1.事务的概念
事务的概念
• 上层看起来比较简单的需求可能对应的后端要做很多工作后端这些工作组合起来才是一个完整的需求解决的方案。
• 事务由一条或多条SQL语句组成这些语句在逻辑上存在相关性共同完成一个任务事务主要用于处理操作量大复杂度高的数据。比如转账就涉及多条SQL语句包括查询余额select、在当前账户上减去指定金额update、在指定账户上加上对应金额update等将这多条SQL语句打包便构成了一个事务。
• MySQL同一时刻可能存在大量事务如果不对这些事务加以控制在执行时就可能会出现问题。比如单个事务内部的某些SQL语句执行失败或是多个事务同时访问同一份数据导致数据不一致的问题。因此一个完整的事务并不是简单的SQL集合事务还需要满足如下四个属性
• 原子性 一个事务中的所有操作要么全部完成要么全部不完成不会结束在中间某个环节。事务在执行过程中如果发生错误则会自动回滚到事务开始前的状态就像这个事务从来没有执行过一样。
• 持久性 事务处理结束后对数据的修改就是永久的即便系统故障也不会丢失。
• 隔离性 数据库允许多个事务同时访问同一份数据隔离性可以保证多个事务在并发执行时不会因为由于交叉执行而导致数据的不一致事务由多个SQL构成隔离性是防止多个事务的SQL交替执行而导致的数据不一致问题。事务隔离分为不同级别包括读未提交 Read uncommitted 、读提交 read committed 、可重复读 repeatable read 和串行化 Serializable 。
• 一致性 在事务开始之前和事务结束以后数据库的完整型没有被破坏这表示写入的资料必须完全符合所有的预设规则这包含资料的精确度、串联型以及后续数据库可以自发性地完成预定的工作。
原子性、持久性、隔离性是手段一致性是目的。一致性其实是仅停留在上层概念上的也就是说原子性、持久性、隔离性都需要在MySQL内部实现一致性不需要在MySQL内部实现只要将原子性、持久性、隔离性这三点在MySQL内部实现了那么结果就是一致的也就实现了一致性。
上面的四个属性简称ACID
• 原子性Atomicity又称不可分割性。
• 一致性Consistency。
• 隔离性Isolation又称独立性。
• 持久性Durability。注MySQL要提供事务机制注定了MySQL内部编码和数据结构的支持。MySQL一定会同时存在多个事务MySQL要对多个事务进行管理工作需要先描述再组织因此事务不要抽象的理解它事务最终一定是要以某种数据结构+算法管理起来。
为什么会出现事务
• 事务本身不是数据库类软件天然有的事务被MySQL编写者设计出来本质是为了当应用程序访问数据库的时候事务能够简化我们的编程模型不需要用户自己去考虑各种各样的潜在错误和并发问题。
• 如果MySQL只是单纯的提供数据存储服务那么用户在访问数据库时就需要自行考虑各种潜在问题包括网络异常、服务器宕机等。因此事务本质是为了应用服务的而不是伴随着数据库系统天生就有的。
2.事务的版本支持
事务的版本支持
• 在MySQL中只有使用了Innodb数据库引擎的数据库或表才支持事务MyISAM不支持。
通过 show engines 命令可以查看数据库引擎。如下
说明一下
• Engine 表示存储引擎的名称。
• Support 表示服务器对存储引擎的支持级别YES表示支持NO表示不支持DEFAULT表示数据库默认使用的存储引擎DISABLED表示支持引擎但已将其禁用。
• Comment 表示存储引擎的简要说明。
• Transactions 表示存储引擎是否支持事务可以看到InnoDB存储引擎支持事务而MyISAM存储引擎不支持事务。
• XA 表示存储引擎是否支持XA事务。
• Savepoints 表示存储引擎是否支持保存点。
3.事务的提交方式
3.1.查看事务的提交方式
查看事务的提交方式
事务常见的提交方式有两种分别是自动提交和手动提交。
通过show命令查看autocommit全局变量可以查看事务的自动提交是否被打开。如下
说明一下autocommit的值为ON表示自动提交被打开值为OFF表示自动提交被关闭即事务的提交方式为手动提交。
3.2.设置事务的提交方式
设置事务的提交方式
通过set命令设置autocommit全局变量的值可以打开或关闭事务的自动提交。如下
说明一下将autocommit的值设置为1表示打开自动提交设置为0表示关闭自动提交相当于将事务提交方式设置为手动提交。
4.事务的相关演示
准备测试表
为了便于演示我们将MySQL的隔离级别设置成读未提交也就是把隔离级别设置的比较低方便看到实验现象。如下
需要注意的是设置全局隔离级别后当前会话的隔离级别不会改变只会影响后续与MySQL新建立的连接因此需要重启终端才能看到会话的隔离级别被成功设置。如下
创建一个银行用户表表中包含用户的id、姓名和账户余额。如下
4.1.演示一证明事务的开始与回滚
演示一证明事务的开始与回滚
启动两个终端左终端使用 begin 或 start transaction 命令启动一个事务右终端查看银行用户表中的信息。如下
左终端中的事务向表中插入一条记录由于我们将隔离级别设置成了读未提交因此在左终端中的事务使用commit提交之前在右终端中就能查看到事务向表中插入的记录。如下
左终端中的事务使用savepoint命令创建一个保存点然后继续向表中插入一条记录这时在右终端中也能看到新插入的这条记录。如下
左终端中的事务使用rollback命令回滚到保存点这时右终端在查看表中数据时就看不到刚才插入的第二条记录了。如下
左终端中的事务使用rollback命令回滚到事务最开始这时右终端在查看表中数据时就看不到任何记录了。如下
最后结束事务使用 commit 命令提交事务如下
说明一下
• 使用 begin 或 start transaction 命令可以启动一个事务。
• 使用 savepoint 保存点 命令可以在事务中创建指定名称的保存点。
• 使用 rollback to 保存点 命令可以让事务回滚到指定保存点。
• 使用 rollback 命令可以直接让事务回滚到最开始。
• 使用 commit 命令可以提交事务提交事务后就不能回滚了。注
1.先设置s1保存点然后进行一些操作后设置s2保存点如果此时回滚到s1保存点那么就无法回滚到s2保存点了因为回滚到s1保存点后s1保存点后面的操作都不存在了包括设置s2保存点的操作。
2.如果没有设置保存点也可以直接使用 rollback 回滚(前提是事务还没有提交)但只能回滚到事务的开始。如果一个事务被提交了commit则不可以回退rollback。
3.事务中所谓的commit提交并不是将数据刷新到磁盘中刷新到磁盘的过程是mysqld自己执行的commit提交其实是将对应数据交付给了mysqld。
MySQL采用两段式提交第一次是commit提交将数据交付给mysqld第二次是mysqld按照一定规则将数据刷新到磁盘中。
4.2.演示二原子性
演示二原子性
在左终端中启动一个事务在右终端查看银行用户表中的信息。如下
左终端中的事务向表中插入一条记录由于隔离级别是读未提交因此在右终端中能够查询到插入的这条记录。如下
如果左终端中的事务在提交之前因为某些原因与MySQL断开连接这里可以使用 quit 命令退出或使用 ctrl + \ 命令让程序崩溃那么MySQL会自动让事务回滚到最开始这时右终端中就看不到之前插入的记录了。如下
因此事务可以手动回滚同时当操作异常MySQL会自动回滚。
4.3.演示三持久性
演示三持久性
在左终端中启动一个事务在右终端查看银行用户表中的信息。如下
左终端中的事务向表中插入一条记录由于隔离级别是读未提交因此在右终端中能够查询到插入的这条记录。如下
左终端中的事务在commit提交后与MySQL断开连接这时右终端中仍然可以看到之前插入的记录因为事务提交后数据就被持久化了。如下
4.4.演示四begin会自动更改提交方式
演示四begin会自动更改提交方式
通过show命令查看autocommit的值为ON表示事务的提交方式是自动提交此时银行用户表中有一条记录。如下
在左终端中启动一个事务并向表中新插入一条记录由于隔离级别是读未提交因此在右终端中能够查询到新插入的这条记录。如下
如果左终端中的事务在提交之前与MySQL断开连接那么MySQL依旧会自动让事务回滚到最开始这时右终端中就看不到之前新插入的记录了。如下
也就是说使用begin或start transaction命令启动的事务都必须要使用commit命令手动提交数据才会被持久化与是否设置autocommit无关。
因此在beginstart transaction和commit之间的提交方式一定为手动提交即begin会自动更改提交方式为手动提交。
4.5.演示五单条SQL与事务的关系
演示五单条SQL与事务的关系
• 实际全局变量autocommit是否被设置影响的是单条SQL语句InnoDB中的每一条SQL都会默认被封装成事务只不过该事务内只有一条SQL语句。
• autocommit为ON则单条SQL语句执行后会自动被提交如果为OFF则SQL语句执行后需要使用commit进行手动提交。
比如通过show命令查看autocommit的值为ON表示事务的提交方式是自动提交此时银行用户表中有一条记录。如下在左终端中直接向表中新插入一条记录由于隔离级别是读未提交因此在右终端中肯定能够查询到新插入的这条记录。如下
但就算左终端在执行单条SQL后不使用commit进行提交而直接与MySQL断开连接这时右终端仍然可以看到之前新插入的记录了因为单条SQL在执行后被自动提交持久化了。如下
相反如果将autocommit设置为OFF表示事务执行后需要手动提交此时银行用户表中有两条记录。如下
在左终端中直接向表中新插入一条记录由于隔离级别是读未提交因此在右终端中肯定能够查询到新插入的这条记录。如下
但如果此时左终端在执行单条SQL后不使用commit进行提交而直接与MySQL断开连接那么这时右终端中就看不到之前新插入的记录了因为这时单条SQL执行后需要使用commit手动提交后才会持久化在commit之前与MySQL断开连接则会自动进行回滚操作。如下
也就是说实际我们之前一直都在使用单SQL事务只不过autocommit默认是打开的因此单SQL事务执行后自动就被提交了。
5.事务的隔离级别
5.1.理解隔离性
理解隔离性
• MySQL服务可能会同时被多个客户端进程线程访问访问的方式以事务的方式进行。
• 一个事务可能由多条SQL语句构成也就意味着任何一个事务都有执行前、执行中和执行后三个阶段而所谓的原子性就是让用户层要么看到执行前要么看到执行后执行中如果出现问题可以随时进行回滚所以单个事务对用户表现出来的特性就是原子性。
• 但毕竟每个事务都有一个执行的过程在多个事务各自执行自己的多条SQL时仍然可能会出现互相影响的情况比如多个事务同时访问同一张表甚至是表中的同一条记录。
• 数据库为了保证事务执行过程中尽量不受干扰于是出现了隔离性的概念而数据库为了允许事务在执行过程中受到不同程度的干扰于是出现了隔离级别的概念。
数据库事务的隔离级别有以下四种
• 读未提交Read Uncommitted 在该隔离级别下所有的事务都可以看到其他事务没有提交的执行结果实际生产中不可能使用这种隔离级别因为这种隔离级别相当于没有任何隔离性会存在很多并发问题如脏读、幻读、不可重复读等。
• 读提交Read Committed 该隔离级别是大多数数据库的默认隔离级别但它不是MySQL默认的隔离级别它满足了隔离的简单定义一个事务只能看到其他已经提交的事务所做的改变但这种隔离级别存在不可重复读和幻读的问题。
• 可重复读Repeatable Read 这是MySQL默认的隔离级别该隔离级别确保同一个事务在执行过程中多次读取操作数据时会看到同样的数据即解决了不可重复读的问题但这种隔离级别下仍然存在幻读的问题。
• 串行化Serializable 这是事务的最高隔离级别该隔离级别通过强制事务排序使之不可能相互冲突从而解决了幻读问题。它在每个读的数据行上面加上共享锁但是可能会导致超时和锁竞争问题这种隔离级别太极端实际生成中基本不使用。注
1.隔离级别隔离性读未提交<读提交<可重复读<串行化。
2.虽然数据库事务的隔离级别有以上四种但一个稳态的数据库只会选择这其中的一种作为自己的默认隔离级别。但数据库默认的隔离级别有时可能并不满足上层的业务需求因此数据库提供了这四种隔离级别可以让我们自行设置。
3.隔离级别基本上都是通过加锁的方式实现的不同的隔离级别对锁的使用是不同的常见的有表锁、行锁、写锁、间隙锁GAP、Next-Key锁GAP+行锁等。
5.2.查看与设置隔离级别
查看全局隔离级别
通过 select @@global.tx_isolation 命令可以查看全局隔离级别。如下
查看会话隔离级别
通过 select @@session.tx_isolation 命令可以查看当前会话的隔离级别。如下
此外通过 select @@tx_isolation 命令也可以查看当前会话的隔离级别。如下
设置会话隔离级别
通过 set session transaction isolation level 隔离级别 命令可以设置当前会话的隔离级别。如下
注设置会话的隔离级别只会影响当前会话新起的会话依旧采用全局隔离级。
设置全局隔离级别
通过 set global transaction isolation level 隔离级别 命令可以设置全局隔离级别。如下
注
1.设置全局隔离级别会影响后续的新会话但当前会话的隔离级别没有发生变化如果要让当前会话的隔离级别也改变则需要重启会话。
2.设置完全局隔离级别并重启后数据库中所有的会话隔离级别都会重新读取并初始化为该全局隔离级别相当于将该全局隔离级别作用在了数据库的所有会话中。
3.如果使用 systemctl restart mysqld 命令将MySQL数据库重启那么数据库会重新从配置文件中读取默认的全局隔离级别将原本的全局隔离级别覆盖进而数据库中所有的会话隔离级别重新读取该默认的全局隔离级别并作用在了数据库的所有会话中。
5.3.读未提交Read Uncommitted
启动两个终端将隔离级别都设置为读未提交并查看此时银行用户表中的数据。如下
在两个终端各自启动一个事务左终端中的事务所作的修改在没有提交之前右终端中的事务就已经能够看到了。如下
说明一下
• 读未提交是事务的最低隔离级别几乎没有加锁虽然效率高但是问题比较多所以严重不建议使用。
• 一个事务在执行过程中读取到另一个执行中的事务所做的修改但是该事务还没有进行提交这种现象叫做脏读。
5.4.读提交Read Committed
启动两个终端将隔离级别都设置为读提交并查看此时银行用户表中的数据。如下
在两个终端各自启动一个事务左终端中的事务所作的修改在没有提交之前右终端中的事务无法看到。如下
只有当左终端中的事务提交后右终端中的事务才能看到修改后的数据。如下
说明一下
• 一个事务在执行过程中两个相同的select查询得到了不同的数据这种现象叫做不可重复读。不可重复读现象会使得同一个事务在select读取时因为select读取的时间点不同而读到不同的内容。
• 不可重复读现象是有问题的下面举例进行解释。
公司年终要根据不同工资区间的员工发放不同的年终礼品因此程序员A要根据公司不同的工资区间对员工进行筛选当程序员A启动一个事务正在筛选的同时程序员B迅速将自己的工资做了调整由工资区间1跳到了工资区间2并commit提交。假设程序员B调整前程序员A筛选了工资区间1的员工里面包含程序员B程序员B调整后程序员A刚好准备筛选工资区间2的员工此时里面仍然包含程序员B这样就出现了问题。
5.5.可重复读Repeatable Read
启动两个终端将隔离级别都设置为可重复读并查看此时银行用户表中的数据。如下
在两个终端各自启动一个事务左终端中的事务所作的修改在没有提交之前右终端中的事务无法看到。如下
并且当左终端中的事务提交后右终端中的事务仍然看不到修改后的数据。如下
只有当右终端中的事务提交后再查看表中的数据这时才能看到修改后的数据。如下
说明一下
• 在可重复读隔离级别下一个事务在执行过程中相同的select查询得到的是相同的数据这就是所谓的可重复读。
• 一般的数据库这里不包括MySQL数据库在可重复读隔离级别下update、insert、delete数据是满足可重复读的但insert数据会存在幻读问题因为隔离性是通过对数据加锁完成的而新插入的数据原本是不存在的因此一般的加锁无法屏蔽这类问题。MySQL数据库的可重复读隔离级别下不存在幻读问题其原理后面会讲。
• 一个事务在执行过程中相同的select查询多次查询得到了新的数据如同出现了幻觉这种现象叫做幻读。
MySQL解决了可重复读隔离级别下的幻读问题比如重新在这两个终端各自启动一个事务左终端中的事务向表中插入数据的在没有提交之前右终端中的事务无法看到。如下并且当左终端中的事务提交后右终端中的事务仍然看不到新插入的数据。如下
只有当右终端中的事务提交后再查看表中的数据这时才能看到新插入的数据。如下
说明一下
• MySQL是通过Next-Key锁GAP+行锁来解决幻读问题的。
5.6.串行化Serializable
启动两个终端将隔离级别都设置为串行化并查看此时银行用户表中的数据。如下
在两个终端各自启动一个事务如果这两个事务都对表进行的是读操作那么这两个事务可以并发执行不会被阻塞。如下
但如果这两个事务中有一个事务要对表进行写操作那么这个事务就会立即被阻塞。如下
直到访问这张表的其他事务都提交后这个被阻塞的事务才会被唤醒然后才能对表进行修改操作。如下
说明一下
• 串行化是事务的最高隔离级别多个事务同时进行读操作时加的是共享锁因此可以并发执行读操作但一旦需要进行写操作就会进行串行化效率很低几乎不会使用。
5.7.隔离级别总结
对MySQL中的隔离级别总结如下
√会发生该问题 X不会发生该问题
说明一下
• 隔离级别越严格安全性越高但数据库的并发性能也就越低在选择隔离级别时往往需要在两者之间找一个平衡点。
• 不可重复读的重点是修改和删除同样的条件 你读取过的数据 再次读取出来发现值不一样了。幻读的重点在于新增同样的条件 第 1 次和第 2 次读出来的记录数不一样。• 表中只写出了各种隔离级别下进行读操作时是否需要加锁因为无论哪种隔离级别只要需要进行写操作就一定需要加锁。
• 上面的例子可以看出事务也有长短事务这样的概念。事务间互相影响指的是事务在并行执行的时候即都没有commit 的时候影响会比较大。• mysql 默认的隔离级别是可重复读一般情况下不要修改。
6.关于一致性
事务执行的结果必须使数据库从一个一致性状态变到另一个一致性状态当数据库只包含事务成功提交的结果时数据库就处于一致性状态。
• 事务在执行过程中如果发生错误则需要自动回滚到事务最开始的状态就像这个事务从来没有执行过一样即一致性需要原子性来保证。
• 事务处理结束后对数据的修改必须是永久的即便系统故障也不能丢失即一致性需要持久性来保证。
• 多个事务同时访问同一份数据时必须保证这多个事务在并发执行时不会因为由于交叉执行而导致数据的不一致即一致性需要隔离性来保证。
• 此外一致性与用户的业务逻辑强相关如果用户本身的业务逻辑有问题最终也会让数据库处于一种不一致的状态。
也就是说一致性实际是数据库最终要达到的效果一致性不仅需要原子性、持久性和隔离性来保证还需要上层用户编写出正确的业务逻辑。
7.多版本并发控制
7.1.数据库的并发场景
数据库并发的场景无非如下三种
• 读-读并发不存在任何问题也不需要并发控制。
• 读-写并发有线程安全问题可能会存在事务隔离性问题可能遇到脏读、幻读、不可重复读。
• 写-写并发有线程安全问题可能会存在两类更新丢失问题。
说明一下• 写-写并发场景下的第一类更新丢失又叫做回滚丢失即一个事务的回滚把另一个已经提交的事务更新的数据覆盖了第二类更新丢失又叫做覆盖丢失即一个事务的提交把另一个已经提交的事务更新的数据覆盖了。
• 读-读并发不需要进行并发控制写-写并发实际也就是对数据进行加锁这里最值得讨论的是读-写并发读-写并发是数据库当中最高频的场景在解决读-写并发时不仅需要考虑线程安全问题还需要考虑并发的性能问题。
7.2.多版本并发控制概念
• 多版本并发控制Multi-Version Concurrency ControlMVCC是一种用来解决读写冲突的无锁并发控制主要依赖记录中的3个隐藏字段、undo日志和Read View实现。
• 为事务分配单向增长的事务ID为每个修改保存一个版本将版本与事务ID相关联读操作只读该事务开始前的数据库快照。
• MVCC保证读写并发时读操作不会阻塞写操作写操作也不会阻塞读操作提高了数据库并发读写的性能同时还可以解决脏读、幻读和不可重复读等事务隔离性问题。
7.3.记录中的3个隐藏字段
数据库表中的每条记录都会有如下3个隐藏字段
• DB_TRX_ID6字节创建插入或最近一次修改该记录的事务ID。
• DB_ROW_ID6字节隐含的自增ID隐藏主键。
• DB_ROLL_PTR7字节回滚指针指向这条记录的上一个版本。
说明一下• 采用InnoDB存储引擎建立的每张表都会有一个主键如果用户没有设置InnoDB就会自动以DB_ROW_ID产生一个聚簇索引。
• 此外数据库表中的每条记录还有一个删除flag隐藏字段用于表示该条记录是否被删除便于进行数据回滚。
示例
创建一个学生表表中包含学生的姓名和年龄。如下
当向表中插入一条记录后该记录不仅包含name和age字段还包含三个隐藏字段。如下
说明一下
• 假设插入该记录的事务的事务ID为9那么该记录的DB_TRX_ID字段填的就是9。
• 因为这是插入的第一条记录所以隐式主键DB_ROW_ID字段填的就是1。
• 由于这条记录是新插入的没有历史版本所以回滚指针DB_ROLL_PTR的值设置为null。
• MVCC重点需要的就是这三个隐藏字段实际还有其他隐藏字段只不过没有画出。• 这里只插入展示了一条记录如果有很多记录那么每条记录的后面都会跟这些隐藏字段。
7.4.undo日志
MySQL的三大日志如下
• redo log重做日志用于MySQL崩溃后进行数据恢复保证数据的持久性。
• bin log逻辑日志用于主从数据备份时进行数据同步保证数据的一致性。
• undo log回滚日志用于对已经执行的操作进行回滚保证事务的原子性。
MySQL会为上述三大日志开辟对应的缓冲区一段内存空间与buffer pool并列用于存储日志相关的信息必要时会将缓冲区中的数据刷新到磁盘。说明一下
• MVCC的实现主要依赖三大日志中的undo log记录的历史版本就是存储在undo log对应的缓冲区中的。
• 不要仅仅认为日志这个东西就是执行记录MySQL的日志是具有功能性和数据/sql保存的能力的。
7.5.模拟MVCC和快照的概念
现在有一个事务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 快照读
• 当前读读取最新的记录就叫做当前读。
• 快照读读取历史版本就叫做快照读。事务在进行增删查改的时候并不是都需要进行加锁保护
• 事务对数据进行增删改的时候操作的都是最新记录即当前读需要进行加锁保护。
• 事务在进行select查询的时候既可能是当前读也可能是快照读如果是当前读那也需要进行加锁保护但如果是快照读那就不需要加锁因为历史版本不会被修改也就是可以并发执行读-写可以并发执行提高了效率这也就是MVCC的意义所在。
而select查询时应该进行当前读还是快照读则是由隔离级别决定的在读未提交和串行化隔离级别下进行的都是当前读而在读提交和可重复读隔离级别下既可能进行当前读也可能进行快照读。
undo log中的版本链何时才会被清除
• 在undo log中形成的版本链不仅仅是为了进行回滚操作其他事务在执行过程中也可能读取版本链中的某个版本也就是快照读。
• 因此只有当某条记录的最新版本已经修改并提交并且此时没有其他事务与该记录的历史版本有关了这时当调用commit提交后该记录在undo log中的版本链才可以被清除也就是说当一个事务commit提交后并且没有其他事务与该记录的历史版本有关了该记录在undo log中的版本链才会被清除。• undo log是给一个事务在其还没有提交前所使用的。
说明一下• 对于新插入的记录来说没有其他事务会访问它的历史版本因此新插入的记录在提交后就可以将undo log中的版本链清除了。
• 因此版本链在undo log中可能会存在很长时间尤其是有其他事务和这个版本链相关联的时候但这也没有坏处这说明它是一个热数据。
7.6.Read View
问题为什么要有隔离级别呢答事务都是原子的所以无论如何事务总有先有后。但是经过上面的操作我们发现事务从 begin->CURD->commit 是有一个阶段的也就是事务有执行前执行中执行后的阶段。但不管怎么启动多个事务总是有先有后的。那么多个事务在执行中 CURD 操作是会交织在一起的。为了保证事务的 “ 有先有后 ” 就应该让不同的事务看到它该看到的内容这就是所谓的隔离性与隔离级别要解决的问题。先来的事务不应该看到后来的事务所做的修改那么如何保证不同的事务看到不同的内容呢也就是如何实现隔离级别呢 答案是Read View。
Read View
• 事务在进行快照读操作时会生成读视图Read View在该事务执行快照读的那一刻会生成数据库系统当前的一个快照记录并维护系统当前活跃的事务ID当每个事务开启时都会被分配一个ID这个ID是递增的所以最新的事务ID值越大事务到来的先后就是由事务ID区分的。
• Read View在MySQL源码中就是一个类本质是用来进行可见性判断的当事务对某个记录执行快照读的时候对该记录创建一个Read View根据这个Read View来判断当前事务能够看到该记录的哪个版本的数据。
注当使用begin启动事务的时候是没有read view的但肯定有事务ID和事务对象当首次进行select的时候mysqld会自动形成read view。
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整个系统已经分配的事务ID值最大值+1。
• m_creator_trx_id 记录创建该Read View的事务的事务ID。
由于事务ID是单向增长的因此根据Read View中的m_up_limit_id和m_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_id和m_low_limit_id之间的事务在生成Read View时可能正处于活跃状态也可能已经提交了这时需要通过判断事务ID是否存在于m_ids中来判断该事务是否已经提交。
示意图如下• 一个事务在进行读操作时只应该看到自己或已经提交的事务所作的修改因此我们可以根据Read View来判断当前事务能否看到另一个事务所作的修改。
• 版本链中的每个版本的记录都有自己的DB_TRX_ID即创建或最近一次修改该记录的事务ID因此可以依次遍历版本链中的各个版本通过Read View来判断当前事务能否看到这个版本如果不能则继续遍历下一个版本。
流程图如下图所示
注上图中的 creator_trx_id 是本事务ID。
源码策略如下
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判断当前事务能否看到这个版本。
举例说明
假设当前有条记录事务操作
事务1、2、3、4开始后事务四首先对记录进行修改并提交然后事务2进行select读取。
事务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
因为事务4先提交事务2再形成快照所以当前事务2能看到版本链中的哪一个我们从DB_TRX_ID=4开始找如下图所示。
我们的事务 2 在快照读该行记录的时候就会拿该行记录的 DB_TRX_ID 去跟 up_limit_idlow_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_ID4< up_limit_id1 ? 不小于下一步 DB_TRX_ID4>= low_limit_id(5) ? 不大于下一步 m_ids.contains(DB_TRX_ID) ? 不包含说明事务4不在当前的活跃事务中。 //结论 故事务4的更改应该看到。 所以事务2能读到的最新数据记录是事务4所提交的版本而事务4提交的版本也是全局角度上最新的版本
举例验证准备测试表如下图所示。在两个终端各自启动一个事务 左边终端模拟上面的事务2右边终端模拟上面的事务4。在右边终端将name(张三)改为name(李四)然后提交在左边终端可以看到修改后的记录内容如下
8.可重复读RR与读提交RC的本质区别
现象演示
启动两个终端将隔离级别都设置为可重复读并查看此时银行用户表中的数据。如下
在两个终端各自启动一个事务在左终端中的事务操作之前先让右终端中的事务查看一下表中的信息。如下
左终端中的事务对表中的信息进行修改并提交右终端中的事务看不到修改后的数据。如下
在右终端中使用 select ... lock in share mode 命令进行当前读可以看到表中的数据确实是被修改了只是右终端中的事务看不到而已。如下
但如果修改一下SQL的执行顺序在两个终端各自启动一个事务后直接让左终端中的事务对表中的信息进行修改并提交然后再让右终端中的事务进行查看这时右终端中的事务就直接看到了修改后的数据。如下
在右终端中使用 select ... lock in share mode 命令进行当前读可以看到刚才读取到的确实是最新的数据。如下
说明一下
• 上面两次实验的唯一区别在于右终端中的事务在左终端中的事务修改数据之前是否进行过快照读。
• 由于RR级别下要求事务内每次读取到的结果必须是相同的因此事务首次进行快照读的地方决定了该事务后续快照读结果的能力。
RR与RC的本质区别
• 正是因为Read View生成时机的不同从而造成了RC和RR级别下快照读的结果的不同。
• 在RR级别下事务第一次进行快照读时会创建一个Read View将当前系统中活跃的事务记录下来此后再进行快照读时就会直接使用这个Read View进行可见性判断因此当前事务看不到第一次快照读之后其他事务所作的修改。
• 而在RC级别下事务每次进行快照读时都会创建一个Read View然后根据这个Read View进行可见性判断因此每次快照读时都能读取到被提交了的最新的数据。
• RR级别下快照读只会创建一次Read View所以RR级别是可重复读的而RC级别下每次快照读都会创建新的Read View所以RC级别是不可重复读的。