MySQL中InnoDB的可重复读是怎么实现的?

首先看一个例子:

CREATE TABLE `test` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `c` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

往test表插入两条数据:

INSERT INTO `test` (`id`, `c`) VALUES (1, 1);
INSERT INTO `test` (`id`, `c`) VALUES (2, 2);

然后我们对这张表做如下操作:

transaction.png

事务的启动机制:begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作InnoDB 表的语句,事务才真正启动。如果我们想马上启动一个事务,可以用 start transaction with consistent snapshot 命令。

第一种启动方式,一致性视图是在执行第一个快照读语句时创建的。

第二种启动方式,一致性视图是在执行 start transaction with consistent snapshot 时创建的。

在MySQL中有两个“视图”的概念:

  • 一个是view。它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建视图的语法是create view...,而它的查询方法和查询表一样。
  • 另一个是InnoDB在实现MVCC时用到的一致性视图,即consistent read view,用于支持读提交(Read Committed)和可重复读(Repeatable Read)隔离级别的实现。

InnoDB是怎么秒级创建快照的

在可重复读隔离级别下,事务启动时就会基于整个数据库创建一个快照。

快照的实现:

快照并不是把数据库所有的数据都拷贝一边存放。

在InnoDB里,每个事务都有一个唯一的transaction id。它是在事务开始的时候向InnoDB的事务系统申请的,是按准许严格递增的。

每行数据都会有多个版本。每次事务更新数据的时候,都会生成一个新的数据版本,并把transaction id 赋值给这个数据版本的事务ID,叫做row trx_id。同时,旧的数据版本也要保留,并在新的数据版本中可以找到旧的数据版本。

简单的说就是,数据表中的一行记录可能有多个版本(row),每个版本有自己的row trx_id。

如图所示:

trx_id.png

图中同一行数据共有四个版本,当前版本是V4,c的值是8,它是被transaction id = 20的事务更新的,所以它的row trx_id也是20。

图中的三个红色箭头就是undo log;其实V1,V2,V3,并不是物理上真实存在的,而是每次更新的时候根据当前版本和undo log 计算出来的。比如,我们想得到V2版本时候的值,就需要从V4依次执行U3,U2得到。

可重复读定义:一个事务开启的时候,能够看到这个时候开启那一刻所有已经提交的事务结果。但开启之后,这个事务commit之前,其他新的事务的更新对它是不可见的。

实现方式:

InnoDB为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前已经启动但还未提交的事务ID

数组里面最小的事务ID标记为低水位,当前系统中已经创建过的事务ID的最大值加1标记为高水位。

注意:低水位是针对事务数组里最小的事务ID,针对的是事务数组;高水位是当前系统创建过的事务ID最大值加1,针对的是当前系统;

这个视图数组和高水位组成了当前事务的一致性视图(read-view)。

数据的可见性规则就是基于数据的row trx_id和一致性视图的对比结果得到的。

data_version.jpg

从图中可以看出,当前事务启动瞬间,一个数据版本的row trx_id有以下几种可能:

  1. 如果落在绿色部分,表示这个版本是已经提交的事务或者是当前事务自己生成的,可见。

  2. 如果落在红色部分,表示这个版本是由将来启动的事务生成的,不可见。

  3. 如果落在黄色部分,包含两种情况:

    a. 如果row trx_id 在数组中,表示这个版本是还没提交的事务生成的,不可见。

    b. 如果row trx_id 不在数组中,表示这个版本是已经提交的事务生成的,可见。

接下来我们分析一下开篇的那个例子,分析事务A返回的结果。

我们可以假设一下:

  1. 事务A开始前,系统里面只有一个活跃事务(启动但为提交)ID=99;
  2. 事务A,B,C的版本号分别为100,101,102,而且当前系统中只有这四个事务;
  3. 三个事务开始前,c=1这行数据的row trx_id是90。

这样,事务 A 的视图数组就是[99,100], 事务 B 的视图数组是[99,100,101], 事务 C 的视图数组是[99,100,101,102]。

transA_select.jpg

从图中可以看出,事务C把c=1改成了c=2,这时候数据版本的row trx_id = 102。

事务B把c=2改成了c=3,这时候数据版本的row trx_id = 101。

可重复读隔离级别下,事务A查询的时候,事务B还没有提交,所以c=3对事务A来说是不可见的,否则就是脏读了。

现在事务A的视图数组是[99,100],读数据都是从当前版本开始读的,所以事务A查询语句读数据流程如下:

  1. 找到c=3的时候,判断row trx_id=101,比高水位大,处于红色区域,不可见;
  2. 继续找历史版本,找到 row trx_id=102,比高水位大,处于红色区域,不可见;
  3. 继续找历史版本,找到row trx_id=90,比低水位小,处于绿色区域,可见。

所以事务A查询得到的结果c=1。

所以一个数据版本,对于一个事务视图来说,除了自己更新的总是可见以外,有三种情况:

  1. 版本未提交,可见;
  2. 版本已提交,但是是在视图创建后提交,不可见;
  3. 版本已提交,但是是在视图创建前提交,可见。

在这个例子中,事务B是在事务C之前启动的,那为什么事务B算出来的c=3呢?

因为事务B在更新之前,是要先读一次当前版本数据的,然后再在当前版本的数据基础之上做的更新,要不然事务C的更新就丢了。

所以这里有一条规则:更新数据都是先读后写,这个读,只能读当前版本的数据,称为“当前读”(current read)。

所以,当事务B更新之前,当前都得到的c=2,更新后生成了新的版本数据c=3,新版本的row trx_id=101,然后事务B查询时拿到的row trx_id=101和自己的版本相同,是自己更新的,可以直接使用,所以事务B查询得到的c=3。

除了update语句是当前读之外,如果select语句加锁,也是当前读。

select c from test where id=1 lock in share mode;
select c from test where id=1 for update;

上面两条语句中,第一条语句加读锁(S锁,共享锁),第二条语句加写锁(X锁,排他锁)。

如果我们改一下例子中的事务A语句,加上lock in share mode或者for update,返回结果c=3.

读提交的逻辑和可重复读的逻辑是类似的,主要的区别就是:

  • 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;
  • 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。

结束!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353