目录
事务的概念
- 事务由一条或多条SQL语句组成,这些语句在逻辑上存在相关性,共同完成一个任务,事务主要用于处理操作量大,复杂度高的数据。比如转账就涉及多条SQL语句,包括查询余额(select)、在当前账户上减去指定金额(update)、在指定账户上加上对应金额(update)等,将这多条SQL语句打包便构成了一个事务。
- MySQL同一时刻可能存在大量事务,如果不对这些事务加以控制,在执行时就可能会出现问题。比如单个事务内部的某些SQL语句执行失败,或是多个事务同时访问同一份数据导致数据不一致的问题。
因此一个完整的事务并不是简单的SQL集合,事务还需要满足四个属性。
- 原子性: 一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中如果发生错误,则会自动回滚到事务开始前的状态,就像这个事务从来没有执行过一样。
- 持久性: 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
- 隔离性: 数据库允许多个事务同时访问同一份数据,隔离性可以保证多个事务在并发执行时,不会因为由于交叉执行而导致数据的不一致。
- 一致性: 在事务开始之前和事务结束以后,数据库的完整型没有被破坏,这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联型以及后续数据库可以自发性地完成预定的工作。
上面的四个属性简称 ACID。
- 原子性(Atomicity,又称不可分割性)。
- 一致性(Consistency)。
- 隔离性(Isolation,又称独立性)。
- 持久性(Durability)。
为什么会出现事务?
- 事务被MySQL编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要用户自己去考虑各种各样的潜在错误和并发问题。
- 如果MySQL只是单纯的提供数据存储服务,那么用户在访问数据库时就需要自行考虑各种潜在问题,包括网络异常、服务器宕机等。因此事务本质是为了应用服务的,而不是伴随着数据库系统天生就有的。
事务的版本支持
通过
show engines
命令可以查看
Engine | 表示存储引擎的名称 |
Support | 表示服务器对存储引擎的支持级别,YES表示支持,NO表示不支持,DEFAULT表示数据库默认使用的存储引擎,DISABLED表示支持引擎但已将其禁用 |
Comment | 表示存储引擎的简要说明 |
Transactions | 表示存储引擎是否支持事务,可以看到InnoDB存储引擎支持事务,而MyISAM存储引擎不支持事务 |
XA | 表示存储引擎是否支持XA事务 |
Savepoints | 表示存储引擎是否支持保存点 |
事务的提交方式
查看事务的提交方式
事务常见的提交方式有两种,分别是 自动提交 和 手动提交
通过 show 命令查看 autocommit 全局变量,可以查看事务的自动提交是否被打开
show variables like 'autocommit';
autocommit 的值为ON表示自动提交被打开,值为OFF表示自动提交被关闭,即事务的提交方式为手动提交
设置事务的提交方式
通过 set 命令设置 autocommit 全局变量的值,可以打开或关闭事务的自动提交
SET AUTOCOMMIT=0; #SET AUTOCOMMIT=0 禁止自动提交
SET AUTOCOMMIT=1; #SET AUTOCOMMIT=1 开启自动提交
事务常见操作方式
提前准备
- 为了便于演示,我们将mysql的默认隔离级别设置成读未提交。
- 具体操作我们后面专门会讲,现在已使用为主。
set global transaction isolation level READ UNCOMMITTED;
需要重启终端,进行查看 !!
创建测试表
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;
正常演示 - 证明事务的开始与回滚
show variables like 'autocommit'; -- 查看事务是否自动提交。我们故意设置成动提交,看看该选项是否影响begin
start transaction; -- 开始一个事务begin也可以,推荐begin
savepoint save1; -- 创建一个保存点save1
insert into account values (1, '张三', 100); -- 插入一条记录
savepoint save2; -- 创建一个保存点save2
insert into account values (2, '李四', 10000); -- 在插入一条记录
select * from account; -- 两条记录都在了
rollback to save2; -- 回滚到保存点save2
select * from account; -- 一条记录没有了
rollback; -- 直接rollback,回滚在最开始
select * from account; -- 所有刚刚的记录没有了
测试回滚,1、回滚到保存点save2,2、回滚到最开始
非正常演示1 - 证明未commit,客户端崩溃,MySQL自动会回滚(隔离级别设置为读未提交)
--终端A
select * from account; -- 当前表内无数据
show variables like 'autocommit'; -- 依旧自动提交
begin; --开启事务
insert into account values (1, '张三', 100); -- 插入记录
select * from account; --数据已经存在,但没有commit,此时同时查看终端B
Aborted -- ctrl + \ 异常终止MySQL
--终端B
select * from account; --终端A崩溃前
select * from account; --数据自动回滚
非正常演示2 - 证明commit了,客户端崩溃,MySQL数据不会在受影响,已经持久化
终端A
show variables like 'autocommit'; -- 依旧自动提交
select * from account; -- 当前表内无数据
begin; -- 开启事务
insert into account values (1, '张三', 100); -- 插入记录
commit; -- 提交事务
ctrl + \ -- 退出数据库
终端B
select * from account; -- 数据存在了,所以commit的作用是将数据持久化到MySQL中
非正常演示3 - 对比试验。证明begin操作会自动更改提交方式,不会受MySQL是否自动提交影响
终端A
select *from account; -- 查看历史数据
show variables like 'autocommit'; -- 查看事务提交方式
set autocommit=0; -- 关闭自动提交
show variables like 'autocommit'; -- 查看关闭之后结果
begin; -- 开启事务
insert into account values (2, '李四', 10000); -- 插入记录
select *from account; -- 查看插入记录,同时查看终端B
ctrl + \ -- 再次异常终止
终端B
select * from account; -- 终端A崩溃前
select * from account; -- 终端A崩溃后,自动回滚
结论:
- 只要输入begin或者start transaction,事务便必须要通过commit提交,才会持久化,与是 否设置set autocommit无关。
- 事务可以手动回滚,同时,当操作异常,MySQL会自动回滚
- 对于 InnoDB 每一条 SQL 语言都默认封装成事务,自动提交。(select有特殊情况,因为 MySQL 有 MVCC )
- 从上面的例子,我们能看到事务本身的原子性(回滚),持久性(commit)
事务操作注意事项:
- 如果没有设置保存点,也可以回滚,只能回滚到事务的开始。直接使用 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 查看当前会话的隔离级别
set session transaction isolation level 隔离级别命令 设置当前会话的隔离级别
set global transaction isolation level 隔离级别命令 设置全局隔离级别
查看隔离级别
设置当前会话的隔离级别
注意: 设置会话的隔离级别只会影响当前会话,新起的会话依旧采用全局隔离级。
注意: 设置全局隔离级别会影响后续的新会话,但当前会话的隔离级别没有发生变化,如果要让当前会话的隔离级别也改变,则需要重启会话。
读未提交(Read Uncommitted)
终端A
set global transaction isolation level read uncommitted; -- 设置隔离级别为 读未提交
quit -- 重启客户端
select @@tx_isolation; -- 查看隔离级别
select * from account;
begin; -- 开启事务
update account set blance=123.0 where id=1; -- 更新指定行
-- 没有commit哦!!!
终端B
begin; -- 开启事务
select * from account; -- 读到终端A更新但是未commit的数据(insert,delete同样)
一个事务在执行中,读到另一个执行中事务的更新(或其他操作)
但是未commit的数据,这种现象叫做脏读(dirty read)
注意:
- 读未提交是事务的最低隔离级别,几乎没有加锁,虽然效率高,但是问题比较多,所以强烈不建议使用。
- 一个事务在执行过程中,读取到另一个执行中的事务所做的修改,但是该事务还没有进行提交,这种现象叫做脏读。
读提交(Read Committed)
启动两个终端,将隔离级别都设置为读提交,并查看此时银行用户表中的数据!!
在两个终端各自启动一个事务,左终端中的事务所作的修改在没有提交之前,右终端中的事务无法看到。
只有当左终端中的事务提交后,右终端中的事务才能看到修改后的数据。
注意:一个事务在执行过程中,两个相同的select查询得到了不同的数据,这种现象叫做不可重复读。
可重复读(Repeatable Read)
启动两个终端,将隔离级别都设置为可重复读,并查看此时银行用户表中的数据!
在两个终端各自启动一个事务,左终端中的事务所作的修改在没有提交之前,右终端中的事务无法看到。
并且当左终端中的事务提交后,右终端中的事务仍然看不到修改后的数据。
只有当右终端中的事务提交后再查看表中的数据,这时才能看到修改后的数据。
注意:
- 在可重复读隔离级别下,一个事务在执行过程中,相同的select查询得到的是相同的数据,这就是所谓的可重复读。
- 一般的数据库在可重复读隔离级别下,update数据是满足可重复读的,但insert数据会存在幻读问题,因为隔离性是通过对数据加锁完成的,而新插入的数据原本是不存在的,因此一般的加锁无法屏蔽这类问题。
- 一个事务在执行过程中,相同的select查询得到了新的数据,如同出现了幻觉,这种现象叫做幻读。
MySQL解决了可重复读隔离级别下的幻读问题,比如重新在这两个终端各自启动一个事务,左终端中的事务向表中插入数据的在没有提交之前,右终端中的事务无法看到。
并且当左终端中的事务提交后,右终端中的事务仍然看不到新插入的数据!!!
只有当右终端中的事务提交后再查看表中的数据,这时才能看到新插入的数据。
MySQL是通过Next-Key锁(GAP+行锁)来解决幻读问题的。
不可重复读和脏读、幻读
幻读(Phantom Read)是数据库事务隔离级别中可能出现的一种现象。
- 在数据库操作中,幻读指的是一个事务在两次查询同一个范围的数据时,第二次查询看到了第一次查询未看到的新行。
- 例如,事务 T1 读取了某些符合条件的行。然后,另一个事务 T2 插入了一些新的符合 T1 查询条件的行。当 T1 再次执行相同条件的查询时,它会发现新的“幻影”行,这些行之前的查询结果中不存在。
- 幻读通常发生在可重复读(Repeatable Read)隔离级别下。在串行化(Serializable)隔离级别下可以避免幻读。
- 为了更好地理解幻读,我们假设一个银行账户表,事务 T1 要统计余额大于 1000 的账户数量。在 T1 执行统计的过程中,事务 T2 新开了一些余额大于 1000 的账户。如果是可重复读隔离级别,当 T1 再次执行相同的统计操作时,就可能得到不同的结果,即出现了幻读。
脏读(Dirty Read)是数据库事务隔离级别中可能出现的一种现象。
- 脏读指的是一个事务读取到了另一个未提交事务修改的数据。
- 比如说,有事务 T1 和事务 T2 。事务 T1 修改了某一行数据,但还没有提交。此时,事务 T2 读取了事务 T1 未提交修改的数据。如果之后事务 T1 回滚了操作,那么事务 T2 读取到的数据就是无效的、“脏”的数据。
- 以下是一个简单的例子来说明脏读:假设有一个账户表,包含账户 ID 和余额两个字段。事务 T1 要将账户 1 的余额从 1000 增加到 2000 ,但还没提交。这时事务 T2 来查询账户 1 的余额 ,事务 T2 读取到了 2000 这个未提交的值。如果之后事务 T1 因为某些原因回滚了,那么事务 T2 读取到的 2000 就是错误的、脏的数据。
- 为了避免脏读,通常会使用更高的事务隔离级别,如可重复读或串行化。
不可重复读(Non-Repeatable Read)是数据库事务隔离级别中可能出现的一种现象。
- 不可重复读指的是在一个事务内,多次读取同一数据集合,但在这个过程中,另一个事务对该数据集合进行了修改或删除操作,导致前后读取的结果不一致。
- 例如,有事务 T1 和事务 T2 。事务 T1 读取了某一行数据,然后事务 T2 对这行数据进行了修改并提交。之后事务 T1 再次读取这行数据时,得到了与第一次不同的值,这就发生了不可重复读。
串行化(Serializable)
启动两个终端,将隔离级别都设置为串行化,并查看此时银行用户表中的数据。
在两个终端各自启动一个事务,如果这两个事务都对表进行的是读操作,那么这两个事务可以并发执行,不会被阻塞。
但如果这两个事务中有一个事务要对表进行写操作,那么这个事务就会立即被阻塞。
直到访问这张表的其他事务都(commit)提交后,这个被阻塞的事务才会被唤醒,然后才能对表进行修改操作。
注意:串行化是事务的最高隔离级别,多个事务同时进行读操作时加的是共享锁,因此可以并发执行读操作,但一旦需要进行写操作,就会进行串行化,效率很低,几乎不会使用。
隔离级别总结
隔离级别 | 脏读 | 不可重复读 | 幻读 | 加锁读 |
读未提交(read uncommitted) | √ | √ | √ | 不加锁 |
读已提交(read committed) | X | √ | √ | 不加锁 |
可重复读(repeatable read) | X | X | X | 不加锁 |
可串行化(serializable) | X | X | X | 加锁 |
注意:
- 隔离级别越严格,安全性越高,但数据库的并发性能也就越低,在选择隔离级别时往往需要在两者之间找一个平衡点。
- 表中只写出了各种隔离级别下进行读操作时是否需要加锁,因为无论哪种隔离级别,只要需要进行写操作就一定需要加锁。
关于一致性
事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态,当数据库只包含事务成功提交的结果时,数据库就处于一致性状态。
- 事务在执行过程中如果发生错误,则需要自动回滚到事务最开始的状态,就像这个事务从来没有执行过一样,即一致性需要原子性来保证。
- 事务处理结束后,对数据的修改必须是永久的,即便系统故障也不能丢失,即一致性需要持久性来保证。
- 多个事务同时访问同一份数据时,必须保证这多个事务在并发执行时,不会因为由于交叉执行而导致数据的不一致,即一致性需要隔离性来保证。
- 此外,一致性与用户的业务逻辑强相关,如果用户本身的业务逻辑有问题,最终也会让数据库处于一种不一致的状态。
也就是说,一致性实际是数据库最终要达到的效果,一致性不仅需要原子性、持久性和隔离性来保证,还需要上层用户编写出正确的业务逻辑。
多版本并发控制
数据库的并发场景
数据库并发的场景无非如下三种:
- 读-读并发:不存在任何问题,也不需要并发控制。
- 读-写并发:有线程安全问题,可能会存在事务隔离性问题,可能遇到脏读、幻读、不可重复读。
- 写-写并发:有线程安全问题,可能会存在两类更新丢失问题。
注意:
- 写-写并发场景下的第一类更新丢失又叫做回滚丢失,即一个事务的回滚把另一个已经提交的事务更新的数据覆盖了,第二类更新丢失又叫做覆盖丢失,即一个事务的提交把另一个已经提交的事务更新的数据覆盖了。
- 读-读并发不需要进行并发控制,写-写并发实际也就是对数据进行加锁,这里最值得讨论的是读-写并发,读-写并发是数据库当中最高频的场景,在解决读-写并发时不仅需要考虑线程安全问题,还需要考虑并发的性能问题。
多版本并发控制
- 多版本并发控制(Multi-Version Concurrency Control,MVCC)是一种用来解决读写冲突的无锁并发控制,主要依赖记录中的3个隐藏字段、undo日志和Read View实现。
- 为事务分配单向增长的事务ID,为每个修改保存一个版本,将版本与事务ID相关联,读操作只读该事务开始前的数据库快照。
- MVCC保证读写并发时,读操作不会阻塞写操作,写操作也不会阻塞读操作,提高了数据库并发读写的性能,同时还可以解决脏读、幻读和不可重复读等事务隔离性问题。
记录中的3个隐藏字段
数据库表中的每条记录都会有如下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变了
比如我们向表中插入一条记录后,该记录不仅包含name和age字段,还包含三个隐藏字段。
- 假设插入该记录的事务的事务ID为9,那么该记录的DB_TRX_ID字段填的就是9。
- 因为这是插入的第一条记录,所以隐式主键DB_ROW_ID字段填的就是1。
- 由于这条记录是新插入的,没有历史版本,所以回滚指针DB_ROLL_PTR的值设置为null。
- MVCC重点需要的就是这三个隐藏字段,实际还有其他隐藏字段,只不过没有画出
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 快照读
- 当前读:读取最新的记录,就叫做当前读。
- 快照读:读取历史版本,就叫做快照读。
事务在进行增删查改的时候,并不是都需要进行加锁保护:
- 事务对数据进行增删改的时候,操作的都是最新记录,即当前读,需要进行加锁保护。
- 事务在进行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来判断,当前事务能够看到该记录的哪个版本的数据。
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是单向增长的,因此根据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来判断当前事务能否看到这个版本,如果不能则继续遍历下一个版本。
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,判断当前事务能否看到这个版本。
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级别是不可重复读的。