1. 启动事务语句
begin/start transaction
语句执行时,并不会立即开启一个事务,在执行他后面第一个操作时,事务才真正启动。
start transaction with consistent snapshot
语句立即开启事务。
视图概念
- 一种是view,是通过语句创建的虚拟表。
- 另一种是InnoDB在实现MVCC时用到的一致性读视图,即consistent read view,用于支持RC(Read Commit)和RR(Repeatable Read)的实现。
2. 可重复读级别下事务隔离实现
在可重复读隔离级别下,事务T启动时候会创建一个视图read-view
,之后事务T执行期间,其他操作对其不受影响。
但是当事务T试图更新某一行数据时,但是这行数据的行锁又被其他事务持有,那么事务T会被阻塞,当持有行锁的事务执行结束,事务T读到的值是如何呢?
2. 1 多个事务场景中的数据可见性
假设有表:
mysql> CREATE TABLE `t` (
`id` int(11) NOT NULL,
`k` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id, k) values(1,1),(2,2);
在以上的表上有如下操作:
事务A | 事务B | 事务C |
---|---|---|
start transaction with consistent snapshot; | ||
start transaction with consistent snapshot; | ||
update t set k = k + 1 where id = 1 | ||
update t set k = k + 1 where id = 1;<br />select k from t where id = 1; | ||
select k from t where id = 1;<br />commit; | ||
commit; |
在以上操作中,事务A看到值为1,事务B看到值为3。
2.2 事务快速启动原理
在可重复读隔离级别下,事务在启动的时候就**“拍了个快照”,这个快照是基于整个数据库的**。但这个过程并不需拷贝整个库的数据。是因为
InnoDB里面每个事务都有一个唯一的事务ID,即transaction id
。是有InnoDB的事务系统严格按照递增顺序生成。
每个事务在对一行数据进行更新时,都会生成一个新的数据版本,即InnoDB中,一行数据有多个数据版本。每次事务更新数据时,会将事务的transaction id
赋值对应的数据版本,记为row trx_id
。
以下示意图为某行数据被多个事务更新之后的状态
InnoDB为每个事务构造一个数组,用来保存这个事务启动瞬间,当前启动了但还没提交的所有事务ID。数组里面事务ID的最小值为低水位,当前系统里面已经创建过的事务ID的最大值加1几位高水位。这个视图数组和高水位,就组成了当前事务的一致性视图。
数据版本的可见性规则就是基于数据的row trx_id
和这个一致性视图的对比结果得到。对于当前事务的启动瞬间来说,一个数据版本的row trx_id
有以下几种可能:
- 小于低水位的id,则标识这个版本的事务已经提交或者是当前事务自己生成,数据可见
- 大于高水位,标识这个版本的事务是由将来的某个事务启动,数据不可见
- 如果介于之间,则标识这个版本是由还没有提交的事务创建的,不可见
比如,对于上图中的数据,如果有一个事务,它的低水位是18,那么当他访问这一行数据时,就会从V4通过U3计算出V3,所以他读到的数据为11。
有了这个逻辑之后,那么系统随后发生的数据更新,都和这个事务无关了,所以这个事务的快照就是“静态”的。InnoDB正是利用了这个特征,实现了“妙计创建快照”的能力。
2.3 事务A查询逻辑
假设2.1的场景中,
1. 系统里面只存在一个活跃事务ID是99
2. 事务A,B,C的版本号分别为100,101,102,且当前系统里面只有这四个事务
3. 三个事务开始前,id为1的记录的数据row trx_id是90
那么,事务A的视图数组就是[99,100],事务B的视图数组是[99,100,101],事务C的视图数组是[99,100,101,102]。
在以上表格的操作中,事务C先把数据从1->2,此时最新的数据版本的row trx_id
为102,然后事务B把数从2->3,这个时候的数据版本的row trx_id
为101.
事务A在查询的时候,当前数据版本101 > 100,数据对事务A不可见,接着找上一个版本,即102,依然不可见,继续找,找到90<99,可见,取出此时的值即 1.
所以一个数据版本,对于一个事务视图来说,除了自己的更新总是可见以外,有三种情况:
- 版本未提交,不可见
- 版本已提交,但是是在视图创建之后提交的,不可见
- 版本已提交,而且是在视图创建之前提交的,可见
2.4 事务B更新逻辑
按照2.2 中的规则,事务B在更时,看到数据应该1才对,因为事务B在更新时,当前的数据版本row trx_id
为102 > 101,应该是不可见的。
但是,InnoDB中更新数据时,遵循另外一个规则:
- 更新数据都是先读读后写的,而且这个读只能读当前的值,成为“当前读”。
因此更新时,拿到的数据是2,而不是1
除了update是用了当前读之外,如果select加了锁也是当前读。例如:
select k from t where id = 1 lock in share mode;
select k from t where id = 1 for update;
如果事务C的操作步骤改成以下情况
事务A | 事务B | 事务C` |
---|---|---|
start transaction with consistent snapshot; | ||
start transaction with consistent snapshot; | ||
start transaction with consistent snapshot;<br />update t set k = k + 1 where id = 1 | ||
update t set k = k + 1 where id = 1;<br />select k from t where id = 1; | ||
select k from t where id = 1;<br />commit; | commit; | |
commit; |
此时,由于两阶段更新的协议,此时事务B的更新操作会被阻塞,直到事务C`提交。
3. 事务的可重复的能力实现总结
可重复读的核心是 一致性读(consistent read);而事务跟新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。
读提交的逻辑和可重复读的逻辑类似,它们的主要的区别为:
- 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图
- 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。
InnoDB的行数据有多个版本,每个数据版本都有自己的row trx_id
,每个事务或者与都有自己的一致性视图。普通查询语句是一致性读,一致性读会根据row trx_id
和一致性视图确定数据版本的可见性。
- 可重复读,查询只承认在事务启动前就已经提交完成的数据
- 读提交,查询值承认在语句启动前就已经提交完成的数据
- 当前读,总是读取已经提交完成的最新版本