MySql-丁奇-学习笔记-MVCC

问题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=1k=3k=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由如下几种可能:

  1. 在绿色部分, 表示这个版本时已经提交的事务或者是当前事务自己生成的, 这个数据是可见的
  2. 在红色部分, 表示这个版本是由将来启动的事务生成的(注意高水位的定义)., 是不可见的
  3. 在黄色部分,有两种情况:
    a. 若DB_TRX_ID在数组中, 表示这个版本是未提交事务生成的, 不可见
    b. 若DB_TRX_ID不在数组中, 表示这个版本是已提交事务生成的, 可见

举个栗子, 在下面这张图中, 如果有一个事务, 其低水位是33, 那么它访问这行数据时, 就会通过DB_TRX_ID 通过 undo log计算并找出它的前一个版本V3的数据. 在这个事务中, 看到的k值为11.
在这里插入图片描述


回到最开始的栗子,如下图
在这里插入图片描述

解释一下上图的过程

  1. 事务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];
  2. 事务C先更新数据(id, k)为(1, 2)
  3. 事务B更新数据(id, k),由于事务C在事务B之前已经将数据更新为(1, 2),所以事务B更新数据时需要遵循当前写机制(一会再说当前写),所以再事务C的基础上更新数据为(1, 3)
  4. 事务A查询数据,发现当前(最新)数据的trx_id=105,高于事务A数组的高水位,不可见,于是向上查找,发现前一个版本数据的trx_id=180,高于事务A数组的高水位,不可见,于是再向上查找,找到trx_id=90版本的数据,可见,于是事务A查到的数据为(id, k) = (1, 1)
  5. 在事务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。

  1. 在事务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的更新操作会因为得不到行锁而阻塞。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值