朋友们、伙计们,我们又见面了,本期来给大家带来数据库事务的知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!
C 语 言 专 栏:C语言:从入门到精通
数据结构专栏:数据结构
个 人 主 页 :stackY、
C + + 专 栏 :C++
Linux 专 栏 :Linux
目录
1. 什么是事务
之前我们对表中数据的增删查改是基于一个用户操作,如果有多个用户同时操作数据(CRRD),对数据会不会有什么影响呢?
当多个用户对数据库的数据同时操作时,会发生数据不一致的问题,所以对于数据库表的操作(CURD)必须得有以下的这些控制:
- 1. 买票的过程得是原子的
- 2. 买票互相不能影响
- 3. 买完票要永久有效
- 4. 买前,和买后都要是确定的状态
- 事务就是一组DML语句组成,这些语句在逻辑上存在相关性,这一组DML语句要么全部成功,要么全部失败,是一个整体。MySQL提供一种机制,保证我们达到这样的效果。事务还规定不同的客户端看到的数据是不相同的。
- 事务就是要做的或所做的事情,主要用于处理操作量大,复杂度高的数据。需要多条 MySQL 语句构成,那么所有这些操作合起来,就构成了一个事务。
- 简单的理解,事务的本质站在使用者的角度来说就是多条sql语句组成的集合,用来完成特定的任务。
- 一个 MySQL 数据库,可不止你一个事务在运行,同一时刻,甚至有大量的请求被包装成事务,在向 MySQL 服务器发起事务处理请求。而每条事务至少一条 SQL,这样如果大家都访问同样的表数据,在不加保护的情况,就绝对会出现问题。
- 甚至,因为事务由多条 SQL 构成,那么,也会存在执行到一半出错或者不想再执行的情况,那么已经执行的怎么办呢?所以,一个完整的事务,绝对不是简单的 sql 集合,还需要满足如下四个属性:(ACID)
- 原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
- 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
- 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交( Readuncommitted )、读提交( read committed )、可重复读( repeatable read )和串行化( Serializable )
- 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
2. 为什么会有事务
事务被 MySQL 编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题。
可以想一下当我们使用事务时,要么提交,要么回滚,我们不会去考虑网络异常、服务器宕机、同时更改一个数据怎么办,因此事务本质上是为了应用层服务的,而不是伴随着数据库系统天生就有的。
我们后面把 MySQL 中的一行信息,称为一行记录。
3. 事务的版本支持
在 MySQL 中只有使用了 Innodb 存储引擎的数据库或表才支持事务, 而MyISAM 不支持。
查看数据库引擎:-- 查看数据库引擎 mysql> show engines\G -- ..... *************************** 5. row *************************** Engine: MyISAM -- 引擎名称 Support: YES -- 默认引擎 Comment: MyISAM storage engine -- 描述 Transactions: NO -- 是否支持事务 MyISAM不支持事务 XA: NO Savepoints: NO -- 是否支持事务保存点 *************************** 7. row *************************** Engine: InnoDB Support: DEFAULT Comment: Supports transactions, row-level locking, and foreign keys Transactions: YES -- InnoDB支持事务 XA: YES Savepoints: YES 9 rows in set (0.00 sec)
4. 事务的提交方式
事务的提交方式常见的有两种:
- 自动提交
- 手动提交
查看事务提交方式:
mysql> show variables like 'autocommit'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | ON | -- 默认自动提交是打开的 +---------------+-------+ 1 row in set (0.05 sec)
用 set 来改变 MySQL 的提交模式:
mysql> set autocommit=0; -- 置为0表示关闭自动提交 Query OK, 0 rows affected (0.00 sec) mysql> show variables like 'autocommit'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | OFF | +---------------+-------+ 1 row in set (0.00 sec) mysql> set autocommit=1; -- 置为1表示打开自动提交 Query OK, 0 rows affected (0.00 sec) mysql> show variables like 'autocommit'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | ON | +---------------+-------+ 1 row in set (0.01 sec)
5. 事务的操作方式
在操作之前首先把MySQL的默认隔离级别设置为读未提交(后面会详细介绍,先上手使用一下事务)
-- 更改默认隔离级别改为读未提交 mysql> set global transaction isolation level READ UNCOMMITTED; Query OK, 0 rows affected (0.00 sec) -- 更改完之后退出重新登录MySQL mysql> quit Bye -- 重启MySQL -- 重新登录之后再查询 mysql> select @@transaction_isolation; +-------------------------+ | @@transaction_isolation | +-------------------------+ | READ-UNCOMMITTED | +-------------------------+ 1 row in set (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;
5.1 事务的开始与回滚
开始一个事务有两种方式:
-- ① 使用start transaction mysql> start transaction; Query OK, 0 rows affected (0.00 sec) -- ② 使用begin(推荐) mysql> begin; Query OK, 0 rows affected (0.00 sec)
创建保存点:
mysql> savepoint save1; -- 创建一个保存点save1 Query OK, 0 rows affected (0.00 sec)
回滚:
-- 注意:回滚操作要在事务运行期间执行 -- ①回滚到指定的保存点 mysql> rollback to save1; -- rollback to 保存点 Query OK, 0 rows affected (0.03 sec) -- ②回滚到最开始 mysql> rollback; -- 直接rollback,回滚在最开始 Query OK, 0 rows affected (0.00 sec)
示例:
-- 开始事务 mysql> begin; Query OK, 0 rows affected (0.00 sec) -- 创建一个保存点save1 mysql> savepoint save1; Query OK, 0 rows affected (0.00 sec) -- 插入数据 mysql> insert into account values (1, '张三', 100); Query OK, 1 row affected (0.00 sec) -- 创建保存点save2 mysql> savepoint save2; Query OK, 0 rows affected (0.00 sec) -- 继续插入数据 mysql> insert into account values (2, '李四', 10000); Query OK, 1 row affected (0.00 sec) mysql> select * from account; +----+--------+----------+ | id | name | blance | +----+--------+----------+ | 1 | 张三 | 100.00 | | 2 | 李四 | 10000.00 | +----+--------+----------+ 2 rows in set (0.00 sec) -- 回滚到save2 mysql> rollback to save2; Query OK, 0 rows affected (0.00 sec) mysql> select * from account; +----+--------+--------+ | id | name | blance | +----+--------+--------+ | 1 | 张三 | 100.00 | +----+--------+--------+ 1 row in set (0.00 sec) --直接回滚到开头 mysql> rollback; Query OK, 0 rows affected (0.00 sec) mysql> select * from account; Empty set (0.01 sec)
5.2 提交事务
提交事务:
-- 事务开始之后做完CURD必须进行提交操作 mysql> commit; Query OK, 0 rows affected (0.00 sec)
示例:还没来得及提交时,客户端崩溃(隔离级别设置为读未提交)
当事务运行起来之后进行的操作还未commit时客户端异常终止,此时MySQL会自动将数据进行回滚至最开始(隔离级别为读未提交)。
将事务进行提交操作(commit)之后,数据就是持久化的了,此时客户端终止并不会影响数据。
所以commit的作用就是将数据持久化到MySQL中。
5.3 单条SQL与事务
不启动事务,并且关闭自动提交:
只使用SQL时,关闭了自动提交,在客户端崩溃之后,数据还是没有了。
不启动事务,但打开自动提交:
结论:
- 只要输入begin或者start transaction,事务便必须要通过commit提交,才会持久化,与是否设置set autocommit无关。
- 事务可以手动回滚,同时,当操作异常,MySQL会自动回滚。
- 对于 InnoDB 每一条 SQL 语言都默认封装成事务,自动提交。(select有特殊情况,因为MySQL 有 MVCC (后面介绍))。
- 从上面的例子,我们能看到事务本身的原子性(回滚),持久性(commit)。
注意:
- 如果没有设置保存点,也可以回滚,只能回滚到事务的开始。直接使用 rollback(前提是事务还没有提交)。
- 如果一个事务被提交了(commit),则不可以回退(rollback)。
- 可以选择回退到哪个保存点。
- InnoDB 支持事务, MyISAM 不支持事务
6. 事务的隔离性
- MySQL服务可能会同时被多个客户端进程(线程)访问,访问的方式以事务方式进行。
- 一个事务可能由多条SQL构成,也就意味着,任何一个事务,都有执行前,执行中,执行后的阶段。而所谓的原子性,其实就是让用户层,要么看到执行前,要么看到执行后。执行中出现问题,可以随时回滚。所以单个事务,对用户表现出来的特性,就是原子性。
- 但,毕竟所有事务都要有个执行过程,那么在多个事务各自执行多个SQL的时候,就还是有可能会出现互相影响的情况。比如:多个事务同时访问同一张表,甚至同一行数据。
- 就如同家长给你说:你要么别学,要学就学到最好。至于你怎么学,中间有什么困难,家长并不关心。那么你的学习,对家长来讲,就是原子的。那么你学习过程中,很容易受别人干扰,此时,就需要将你的学习隔离开,保证你的学习环境是健康的。
- 所以在事务的场景中,隔离是非常有必要的。
- 数据库中,为了保证事务执行过程中尽量不受干扰,就有了一个重要特征:隔离性
- 数据库中,允许事务受不同程度的干扰,就有了一种重要特征:隔离级别
6.1 事务的隔离级别
- 读未提交【Read Uncommitted】: 在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。(实际生产中不可能使用这种隔离级别的),但是相当于没有任何隔离性,也会有很多并发问题,如脏读,幻读,不可重复读等,上面为了做实验方便,用的就是这个隔离性。
- 读提交【Read Committed】 :该隔离级别是大多数数据库的默认的隔离级别(不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读,即一个事务执行时,如果多次 select, 可能得到不同的结果。
- 可重复读【Repeatable Read】: 这是 MySQL 默认的隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行。但是会有幻读问题。
- 串行化【Serializable】: 这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决了幻读的问题。它在每个读的数据行上面加上共享锁,。但是可能会导致超时和锁竞争(这种隔离级别太极端,实际生产基本不使用)。
隔离级别如何实现:隔离基本都是通过锁实现的,不同的隔离级别,锁的使用是不同的。常见有,表锁,行锁,读锁,写锁,间隙锁(GAP),Next-Key锁(GAP+行锁)等。
① 查看隔离级别:
-- ① 查看全局隔离级别 mysql> select @@global.transaction_isolation; +--------------------------------+ | @@global.transaction_isolation | +--------------------------------+ | REPEATABLE-READ | +--------------------------------+ 1 row in set (0.00 sec) -- ② 查看当前会话终端隔离级别 -- 默认拷贝全局隔离级别也可以自己更改 mysql> select @@session.transaction_isolation; +---------------------------------+ | @@session.transaction_isolation | +---------------------------------+ | REPEATABLE-READ | +---------------------------------+ 1 row in set (0.00 sec) -- ③ 默认同上 mysql> select @@transaction_isolation; +-------------------------+ | @@transaction_isolation | +-------------------------+ | REPEATABLE-READ | +-------------------------+ 1 row in set (0.00 sec)
② 设置隔离级别:
-- 设置全局/当前会话隔离级别 SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
示例:
-- 查看当前会话的隔离级别 mysql> select @@session.transaction_isolation; +---------------------------------+ | @@session.transaction_isolation | +---------------------------------+ | REPEATABLE-READ | +---------------------------------+ 1 row in set (0.00 sec) -- 更改当前会话的隔离级别为串行化 mysql> set session transaction isolation level serializable; Query OK, 0 rows affected (0.00 sec) -- 查看当前会话隔离级别 mysql> select @@session.transaction_isolation; +---------------------------------+ | @@session.transaction_isolation | +---------------------------------+ | SERIALIZABLE | +---------------------------------+ 1 row in set (0.00 sec) -- 更改当前会话隔离级别并不会影响全局隔离级别 mysql> select @@global.transaction_isolation; +--------------------------------+ | @@global.transaction_isolation | +--------------------------------+ | REPEATABLE-READ | +--------------------------------+ 1 row in set (0.00 sec)
-- 设置全局隔离级别为读未提交 mysql> set global transaction isolation level READ UNCOMMITTED; Query OK, 0 rows affected (0.00 sec) mysql> SELECT @@global.transaction_isolation; +--------------------------------+ | @@global.transaction_isolation | +--------------------------------+ | READ-UNCOMMITTED | +--------------------------------+ 1 row in set (0.00 sec) -- 全局隔离级别会影响当前隔离级别 mysql> SELECT @@session.transaction_isolation; +---------------------------------+ | @@session.transaction_isolation | +---------------------------------+ | READ-UNCOMMITTED | +---------------------------------+ 1 row in set (0.00 sec) -- 注意,如果没有现象需要重启MySQL
下面来演示不同的隔离级别情况下多个事务并发访问的问题:
6.1.1 读未提交(Read Uncommitted)
读未提交几乎没有加锁,虽然效率高,但是问题太多,严重不建议采用。
-- 将隔离级别设置为读未提交 mysql> set global transaction isolation level READ UNCOMMITTED; Query OK, 0 rows affected (0.00 sec) -- 重启MySQL客户端即可 mysql> select @@transaction_isolation; +-------------------------+ | @@transaction_isolation | +-------------------------+ | READ-UNCOMMITTED | +-------------------------+ 1 row in set (0.00 sec)
示例演示:
一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未commit的数据,这种现象叫做脏读。
6.1.2 读提交(Read Committed)
-- 设置隔离级别为读提交 mysql> set global transaction isolation level read committed; Query OK, 0 rows affected (0.00 sec) -- 重启MySQL mysql> select @@transaction_isolation; +-------------------------+ | @@transaction_isolation | +-------------------------+ | READ-COMMITTED | +-------------------------+ 1 row in set (0.00 sec)
示例演示:
客户端在这里是并没有结束掉事务的,所以就造成了在一个事务中,同样的读取,在不同的时间段(事务并未结束),读取到了不同的值,这种现象就叫做不可重复读。
不可重复读带来的问题就好比某一家公司根据薪资发放年终奖品,负责发放的员工看到同一个员工的薪资会不一样,这就成为了问题的所在。
6.1.3 可重复读(Repeatable Read)
-- 设置隔离级别为可重复读 mysql> set global transaction isolation level repeatable read; Query OK, 0 rows affected (0.00 sec) --重启MySQL客户端即可 mysql> select @@transaction_isolation; +-------------------------+ | @@transaction_isolation | +-------------------------+ | REPEATABLE-READ | +-------------------------+ 1 row in set (0.00 sec)
示例演示:
在终端B的事务还未结束时,事务无论什么时候进行查找,看到的结果都是一致的,这叫做可重复读!
如果将这里的修改数据换成新插入数据呢?
发现终端A在对应事务中insert的数据,在终端B的事务周期中,也没有什么影响,也符合可重复的特点。但是,一般的数据库在可重复读情况的时候,无法屏蔽其他事务insert的数据,因为隔离性实现是对数据加锁完成的,而insert待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题,会造成虽然大部分内容是可重复读的,但是insert的数据在可重复读
情况被读取出来,导致多次查找时,会多查找出来新的记录,就如同产生了幻觉。这种现象,叫做幻读(phantom read)。很明显,MySQL在RR级别的时候,是解决了幻读问题的(解决的方式是用Next-Key锁(GAP+行锁)解决的。
6.1.4 串行化
对所有操作全部加锁,进行串行化,不会有问题,但是只要串行化,效率很低,几乎完全不会被采用(这里就不演示了)。
总结:
- 其中隔离级别越严格,安全性越高,但数据库的并发性能也就越低,往往需要在两者之间找一个平衡点。
- 不可重复读的重点是修改和删除:同样的条件, 你读取过的数据,再次读取出来发现值不一样了。
- 幻读的重点在于新增:同样的条件,第1次和第2次读出来的记录数不一样。
- MySQL 默认的隔离级别是可重复读,一般情况下不要修改。
- 上面的例子可以看出,事务也有长短事务这样的概念。事务间互相影响,指的是事务在并发执行的时候,即都没有commit的时候,影响会比较大。
7. 事务的一致性
事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。
当数据库只包含事务 成功提交的结果时,数据库处于一致性状态。
如果系统运行发生中断,某个事务尚未完成而被迫中 断,而改未完成的事务对数据库所做的修改已被写入数据库,此时数据库就处于一种不正确(不一 致)的状态。因此一致性是通过原子性来保证的。其实一致性和用户的业务逻辑强相关,一般MySQL提供技术支持,但是一致性还是要用户业务逻辑做支撑,也就是,一致性,是由用户决定的。而技术上,通过事务的原子性、持久性、隔离性从而来保证一致性。
8. 数据库并发的场景
- 读-读:不存在任何问题,也不需要并发控制
- 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
- 写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失(后面补充)
8.1 MVCC
多版本并发控制( MVCC )是一种用来解决 读-写冲突 的无锁并发控制
为事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联,读操作只读该事务开始前的数据库的快照。 所以 MVCC 可以为数据库解决以下问题:
- 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
- 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
理解 MVCC 需要知道三个前提知识:
- 3个记录隐藏字段
- undo 日志
- Read View
根据MVCC来看,每个事务都要有自己的事务ID,可以根据事务ID的大小,决定事务到来的这个先后顺序;
MySQL可能会面临处理多个事务的情况,每一个事务都有自己对应的生命周期,所以MySQL要对多个事务进行管理 -- 先描述,再组织,所以,事务也要有自己的结构体对象。
8.1.1 3个隐藏字段
- DB_TRX_ID :6 byte,最近修改( 修改/插入 )事务ID,记录创建这条记录/最后一次修改该记录的事务ID
- DB_ROLL_PTR : 7 byte,回滚指针,指向这条记录的上一个版本(简单理解成,指向历史版本就行,这些数据一般在 undo log 中)
- DB_ROW_ID : 6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键, InnoDB 会自动以DB_ROW_ID 产生一个聚簇索引
- 补充:实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了
上面说的意思其实就是在表中的一条数据记录并不只有他的表结构,还有这三个隐藏的字段
我们目前并不知道创建该记录的事务ID,隐式主键,我们就默认设置成null,1。第一条记录也没有其他版本,我们设置回滚指针为null。
8.1.2 undo 日志
MySQL 将来是以服务进程的方式,在内存中运行。我们之前所讲的所有机制:索引,事务,隔离性,日志等,都是在内存中完成的,即在 MySQL 内部的相关缓冲区中,保存相关数据,完成各种判断操作。然后在合适的时候,将相关数据刷新到磁盘当中的。所以,我们这里理解undo log,简单理解成,就是 MySQL 中的一段内存缓冲区,用来保存日志数据的就行。
当我们commit之后,undo log 就会被释放掉,所以这也就是不能回滚的原因。
8.1.3 模拟MVCC
示例一:
- ① 现在有一个事务10,对student表中记录进行修改(update):将name(张三)改成name(李四)。因为要修改,所以要先给该记录加行锁。
- ② 修改前,先将修改的行记录拷贝到undo log中,所以,undo log中就有了一行副本数据。(原理就是写时拷贝)
- 所以现在 MySQL 中有两行同样的记录。现在修改原始记录中的name,改成 '李四'。并且修改原始记录的隐藏字段 DB_TRX_ID 为当前 事务10 的ID, 我们默认从 10 开始,之后递增。而原始记录的回滚指针 DB_ROLL_PTR 列,里面写入undo log中副本数据的地址,从而指向副本记录,既表示我的上一个版本就是它。
- 事务10提交,释放锁。
示例二:
- ① 现在又有一个事务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提交,释放锁
这样,我们就有了一个基于链表记录的历史版本链。所谓的回滚,根据回滚指针指向的地址,无非就是用历史数据,覆盖当前数据。
上面的一个一个版本,我们可以称之为一个一个的快照。
注意事项:
① 上述情况是修改(update)的情况,那么如果是删除(delete)的情况呢?
- 删除数据并不是清空,而是将隐藏字段里面的flag设置为删除状态,也是可以形成一个版本的。
② 如果是插入(insert)的情况呢?
- 插入操作也就是之前没有数据,那么insert也就没有历史版本。但是一般为了回滚操作,insert的数据也是要被放入undo log中,如果当前事务commit了,那么这个undo log 的历史insert记录就可以被清空了。
③ 当我们使用select查询时读取的最新版还是历史版本呢?
- 当前读:读取最新的记录,增删改都是当前读的一种,select也有可能当前读,当多个事务同时进行增删改时都是当前读,是要加锁的,那么此时如果有select过来也需要读取最新的版本,那么也需要进行加锁,这就是一种串行化的方式,效率是很低下的,所以需要一种新的方式;
- 快照读:读取历史版本(一般而言),如果读取历史版本时,是不受锁的限制的,也就是可以并发执行,所以这样读取提高了效率,这也就是MVCC的意义所在。
④ 那么是什么决定了select是当前读还是快照读呢?
- 其实就是隔离级别,因为有了隔离级别,事务才具有了原子性,所以无论如何多个事务总有先后,在多事务执行中,CURD操作是交织在一起的,为了保证事务的先后顺序,是需要将不同的事务看到他该看到的内容,这就是所谓的隔离性与隔离级别要解决的问题。
⑤ 如何保证不同的事务看到不同的内容呢?也就是如何实现隔离级别?
- 就需要用到接下来的读视图(Read View)。
8.1.4 Read View
- Read View就是事务进行 快照读 操作的时候生产的 读视图 (Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
- Read View 在 MySQL 源码中,就是一个类,本质是用来进行可见性判断的。 即当我们某个事务执行快照读的时候,对该记录创建一个 Read View 读视图,把它比作条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的 undo log 里面的某个版本的数据。
下面是Read View一个简单的结构用于理解:
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 我们在实际读取数据版本链的时候,是能读取到每一个版本对应的事务ID的,即:当前记录的DB_TRX_ID 。
那么,我们现在手里面有的东西就有,当前快照读的 ReadView 和 版本链中的某一个记录的
DB_TRX_ID 。
所以现在的问题就是,当前快照读,应不应该读到当前版本记录。对应的源码策略:
如果查到不应该看到当前版本,接下来就是遍历下一个版本,直到符合条件,即可以看到。上面的ReadView 是当你进行select的时候,会自动形成。
快照到的事务ID一定是连续的吗?不一定;
比如,这里有1,2,3,4,5号事务,在快照之前2,4提交了,快照找到的就是1,3,5,即如果DB_TRX_ID不在m_ids列表中,说明已经提交!是可以看到的,如果在,则说明该事务和我们的事务都是活跃事务,没有commit,不应该看到。
9. 读提交(RC)与可重复读(RR)的区别
当前读和快照读在RR级别下的区别:
select * from user lock in share mode :以加共享锁方式进行读取,对应的就是当前读。
测试①:
事务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
userselect lock in share mode当前读,读到age=18 select * from user lock in share mode 测试②:
事务A操作 事务A描
述事务B描述 事务B操作 begin 开启事务 开启事务 begin select * from
user快照读,
查到
age=18- - update user set
age=28 where id=1;更新
age=28- - commit 提交事务 - - select快照读,age=28 select * from
userselect lock in share mode当前读,读到age=28 select * from user lock in share mode
- 测试1与测试2:唯一区别仅仅是 测试1 的事务B在事务A修改age前 快照读 过一次age数据而 测试2 的事务B在事务A修改age前没有进行过快照读。
结论:
- 事务中快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读,决定该事务后续快照读结果的能力
- delete同样如此
- Read View是事务可见性的一个类,不是事务创建出来,就会有Read View,而是当这个事务(已经存在),首次进行快读的时候,MySQL形成Read View!
- Read View是一个对象,值初始化之后就不变了。
标签:事务,快照,隔离,--,MySQL,sec,mysql From: https://blog.csdn.net/Yikefore/article/details/145106702RR 与 RC的本质区别:
- 正是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才会有不可重复读问题