问题0
有如下表结构,分析下面三个事务读到的数据是甚么?
create table `t`{
`id` int(11) NOT NULL,
`k` int(11) DEFAULT NULL,
PRIMARY KEY(`id`)
} ENGINE=InnoDB;
insert into t values(1,1);
事务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; select k from t where id=1; | ||
select k from t where id=1; commit; | ||
commit; |
注意
begin/start transaction 命令并不是一个事务的起点,在执行到它们后的第一个操作InnoDB表的语句,事务才真正启动.
如果想要马上启动一个事务,可以使用start transaction with consistent snapshot;
事务C使用自动提交autocommit=1
三个事务结束后最终能读到的数据如下
事务A | 事务B | 事务C |
---|---|---|
k=1 | k=3 | k=2 |
懵逼了吧 ! 那就继续往下看奥 !
首先MySql中有两个视图的概念
- view:一个用查询语句定义的虚拟表,在调用时执行查询语句并生成结果,
create view ... as ...
,可以有只读视图和非只读视图(可修改原数据) - consistent read view:一致性视图,用于支持RC和RR隔离级别的实现
“快照”(一致性视图)在MVCC中时如何工作的?
1. 一致性视图是干嘛用的?
在RR隔离级别下,事务开始时会对整库拍一个"快照",之后的操作都是基于这个快照进行的,所以在RR隔离级别下看不到后来事务提交的数据.
在RC隔离级别下,每条语句执行前都会拍一个"快照",所以能看到后来事务提交的数据.
2. 一致性视图是如何实现的
InnoDB里面每一个事务都有一个唯一的事务ID,即transaction id.它是在事务开始时向InnoDB事务系统申请的,而且是按照申请顺序严格递增的.
每行数据也有多个版本, 每次事务更新数据时都会生成一个新的版本, 并将版本号记录在一个隐藏的数据列DB_TRX_ID
中. 同时旧的数据要保留, 并且在新的数据版本中可以直接拿到它(通过另一个隐藏列DB_ROLL_PT
).
所以一行数据被多个事务连续更新的状态如下图:
上图为一个行数据的四个版本, 当前版本是V4, k=12, 由57号事务更新的, DB_TRX_ID=57.
DB_ROLL_PT相当于一个指针, 指向的就是undo log中的记录, 而不同版本的数据并不是物理上真实存在的, 而是每次需要的时候根据当前版本和undo log计算出来的.
由RR的定义, 一个事务启动的时候, 能够看到所有已经提交的事务结构. 但是之后, 在这个事务执行期间, 其他事务提交的更新是不可见的.
首先事务开始时InnoDB为事务构建了一个数组, 用来保存这个事务启动瞬间,当前所有’活跃’的事务ID, '活跃’是指事务开始了但是还没有提交.
数组里的事务ID的最小值记位低水位
当前系统里面已经创建过的事务ID的最大值加一记位高水位
由这个数组和高低水位,就组成了当前事务的一致性视图.
这个视图把所有的DB_TRX_ID分为了如下几种情况:
这样一来,对于当前事务启动瞬间,一个数据版本的DB_TRX_ID由如下几种可能:
- 在绿色部分, 表示这个版本时已经提交的事务或者是当前事务自己生成的, 这个数据是可见的
- 在红色部分, 表示这个版本是由将来启动的事务生成的(注意高水位的定义)., 是不可见的
- 在黄色部分,有两种情况:
a. 若DB_TRX_ID在数组中, 表示这个版本是未提交事务生成的, 不可见
b. 若DB_TRX_ID不在数组中, 表示这个版本是已提交事务生成的, 可见
举个栗子, 在下面这张图中, 如果有一个事务, 其低水位是33, 那么它访问这行数据时, 就会通过DB_TRX_ID 通过 undo log计算并找出它的前一个版本V3的数据. 在这个事务中, 看到的k值为11.
回到最开始的栗子,如下图
解释一下上图的过程
- 事务A,事务B,事务C依次开启并操作同一行数据,数据库中该行的当前版本为90, (id, k) = (1, 1).
事务A: transaction_id=99, read view 数组: [90, 99];
事务B: transaction_id=105, read view 数组: [90, 99, 105];
事务C: transaction_id=180, read view 数组: [90, 99, 105, 180]; - 事务C先更新数据(id, k)为(1, 2)
- 事务B更新数据(id, k),由于事务C在事务B之前已经将数据更新为(1, 2),所以事务B更新数据时需要遵循当前写机制(一会再说当前写),所以再事务C的基础上更新数据为(1, 3)
- 事务A查询数据,发现当前(最新)数据的trx_id=105,高于事务A数组的高水位,不可见,于是向上查找,发现前一个版本数据的trx_id=180,高于事务A数组的高水位,不可见,于是再向上查找,找到trx_id=90版本的数据,可见,于是事务A查到的数据为(id, k) = (1, 1)
- 在事务B的更新操作前查询数据,得到的也是(1, 1),但是在更新操作后再查询数据,得到的就是(1, 3)了
上述过程中,事务B更新数据时并没有遵循RR隔离级别下的数据一致性视图,这是为甚么呢?
这里就需要介绍当前写机制,InnoDB事务更新数据时需要获取行锁,再更新数据操作前需要拿到数据库中最新版本的数据,再进行更新,也就是当前写。同样的,如果一条查询语句也加了行锁,比如: select k from t where id=1 for update;
或者 select k from t where id=1 lock in share mode;
,也会触发当前写机制。
所以事务B在更新数据时,拿到的最新数据是事务C已经提交的数据,并在此基础上更新k值。
所以上述过程中的第5步的结果是为甚么就清楚了吧:更新操作前读到的是trx_id=90的数据,更新操作后读到的是自己更新的数据,trx_id=180。
- 在事务B的更新操作前查询数据,得到的也是(1, 1),但是在更新操作后再查询数据,得到的就是(1, 3)了
那如果是下面这种情况呢?
事务A | 事务B | 事务C |
---|---|---|
start transaction with consistent snapshot; | ||
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; select k from t where id=1; | ||
commit; | ||
select k from t where id=1; commit; | ||
commit; |
事务C没有开启自动提交,而是在事务B更新操作后提交的数据,这种情况下由于事务C先获得了数据行的写锁,所以事务B必须等待事务C提交并释放了写锁后,获取了行锁才能进行更新操作。
所以得到的结果和之前的栗子是一样的,只不过事务B的更新操作会因为得不到行锁而阻塞。