【原创】你真的理解MySQL InnoDB事务的隔离性吗?

事务

事务保证一组数据库操作要么成功要么失败
当数据库中有多个事务同时进行时,可能会出现脏读(dirty read),不可重复读(non-repeatable read),幻读(phantom read)问题,数据库的事务隔离级别能解决这些问题。

事务隔离级别

SQL 标准的事务隔离级别包括

  • 读未提交(read uncommitted):一个事务还没有提交,他所做的变更能被其他事务看到
  • 读提交(read committed):一个事务提交了之后,他所做的变更才能被其他事务看到
  • 可重复读(repeatable read):一个事务在执行过程中,他所看到的数据总是和在该事务启动时看到的数据视图是一致的。同时,该事务未提交的变更对其他事务也是不可见的
  • 串行读(serializable read):对于同一行数据,读会加上读锁,写会加上写锁,当出现读写锁冲突的时候,后一个事务必须等待前一个事务执行完成才能继续执行
InnoDB 存储引擎的事务隔离级别的实现

数据库会创建视图,访问的时候以视图的逻辑结果为准。
对于 读未提交 ,直接返回记录的最新值,不存在视图的概念;
对于 读提交 ,在每个 SQL 语句开始执行的时候会创建一个视图;
对于 可重复读 ,在每个事务启动时候会创建一个视图,整个事务过程中都使用该视图;
对于 串行读,是直接用锁来避免串行访问的;

那么,这个视图(即快照)是怎样创建与实现的呢?

对于 可重复读 隔离级别,事务在启动的时候会创建快照,这个快照是基于整个库的,这个快照不是拷贝数据库中的所有数据生成的。InnoDB 中每一个事务都有一个唯一的事务ID(transaction id),它是事务开始的时候向 InnoDB 的事务系统申请的,并且是严格自增的(TODO:事务id的值范围,超过了会发生什么),而且数据库的每行数据都有多个版本,每次事务更新数据的时候,都会生成一个新的版本,并且把事务的 transaction id 赋值给这个数据版本的事务 id,记为 row trx_id。同时,旧的数据版本会保留,并且在新的数据版本中,通过 undo log 能够得到旧版本的数据,下面是一个简单的图示:

图一.png

    这一行此时有四个 version,v4 是最新的,它被 transaction id 为 999 的事务更新,因此这个version的 row trx_id 是 999。
    当然,v1,v2,v3 并不是物理上真正存在的,而是需要的时候通过 v4 和 undo log 计算出来的。
    当一个事务启动的瞬间,InnoDB会为该事务构造一个数组,用来保存当前所有活跃的事务(即还没有提交的事务)的 transaction id ,数组中事务 id 最小的被记为低水位,当前系统已经创建过的事务id的最大值加1被记为高水位,这个视图数组和高水位就组成了当前事务的一致性视图,数据版本的可见性就是根据当前事务的id和这个一致性视图的对比结果得到的。
    所以在事务启动的瞬间,一致性视图把当前系统所有的row trx_id 分成了以下几种情况:

图二.png

  • 若落在黄色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的
  • 若落在紫色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的
  • 若落在绿色部分,那包含两种情况
    a) 若row trx_id 在数组中,表示这个版本是由未提交的事务生成的,不可见
    b)若row trx_id 不在数组中,表示这个版本是由已经提交的事务生成的,可见

    因此,对于图一来说,假设一个事务的低水位是 777,那么访问的那一行数据的时候,就会通过v4和undo log计算出v2版本时的值,所以在它看来,这一行的值是 13

    接下来,我们举一个栗子来实践下:

  • 建表:
create TABLE trans_1 (id int(4) not null PRIMARY KEY,k int(4));
insert into trans_1 values(1,1);
  • 事务时序:
                                                                表一
事务A 事务B 事务C
start transaction with consistent snapshot
start transaction with consistent snapshot
update trans_1 set k = k + 1 where id = 1;
update trans_1 set k = k + 1 where id = 1;select * FROM trans_1 where id = 1;
select * FROM trans_1 where id = 1;commit;
commit

我们不妨假设:

  • 事务A开始之前,系统中只有一个活跃的事务,id为 66;
  • 事务A,事务B,事务C 的事务ID分别是 67,68,69;
  • 事务A开始之前,(1,1)这一行数据的数据的row trx_id 为 50;

这样,事务A是视图数组为[66,67],高水位的值是68,事务B的视图数组为[66,67,68],高水位的值是69,事务C的视图数组为[66,67,68,69],高水位的值是70。

事务C 的更新使得id=1这一行的最新版本是 69 了,50 已经成为历史版本,事务B 的更新使得 id=1 这一行的最新版本是 68 , 69 这个成为了历史版本。在事务A进行select的时候,select 的逻辑是:

a):id=1 这一行的最新版本 68,位于高水位,不可见。

b):通过undo log找到上一个版本,即 69 这个版本,比高水位大,不可见

c):再通过 undo log 找到上一个版本,即 50 这个版本,比低水位小,可见

所以 select 出来的就是 50 这个版本时候的值,即 k=1

说了这么多,数据可见性的整体感知就是:

  • 版本未提交,不可见
  • 版本在事务启动(视图数组创建)之后提交,不可见
  • 版本在事务启动之前提交,可见

在事务B 执行 update 之后,select出的 k 的值是3,会不会觉得奇怪呢?

事务B 在 update 之前,select 出 id=1 的 k 值是 1,即事务C 的 update 对事务B 是不可见的,事务B 的 update 应该是在 k=1 的基础上进行的。但为什么 select 出的值是 3 呢?这设计到一个当前读的概念,当更新数据的时候,都是先读后写,而这个读,只能读取当前的值,称为”当前读“。所以事务B update 之前 k 的值是 2 (单独去执行 select 的话 k = 1),update 的时候是以 k =2 为基础的,然后进行 select 的时候,发现数据的最新版本是 68,而自己的版本号也是 68,判断出是自己的更新,可以直接使用,所以 select 出的值就是 3

除了update语句外,如果select语句加上锁也是可以当前读的

如果 事务C update之后没有立即提交,那么情况会是怎样的呢?
                                                            表二

事务A 事务B 事务C ~
start transaction with consistent snapshot;
start transaction with consistent snapshot ;
start transaction with consistent snapshot;update trans_1 set k = k + 1 where id = 1;
update trans_1 set k = k + 1 where id = 1; select * FROM trans_1 where id = 1;
select * FROM trans_1 where id = 1; commit; commit;
commit;

由于事务C update之后没有提交,69 这个版本的写锁还没有释放,当事务B 去update的时候,由于要当前读,必须读取最新的版本,且要加锁,因此事务B就被阻塞了,直到事务C 提交之后,才能继续当前读

读提交 级别下,由于是每一个语句对应一个视图,
对于表一,事务B select的结果是 3,事务A select的结果是 2

ps:如果你的答案不是这个,你可能需要再看一遍文章

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

推荐阅读更多精彩内容