InnoDB 事务隔离机制

1、事务四大特性:ACID

Atomicity 原子性:事务的操作要么一起成功,要么一起失败;

Consistency 一致性:一致性是对数据可见性的约束,一个事务多次操作数据的中间状态对其他事务不可见;

Isolation 隔离性:多个事务并发执行时,一个事务不应该受到其他事务的影响,数据库支持不同的隔离级别来满足不同场景的需求;

Durability 持久性:事务完成之后,所有的操作结果都保存到了数据库之中,不会丢失,不能回滚;

2、事务并发产生的问题

脏读:对于两个事务 T1 和T2,T1读取了已经被T2更新但还没有被提交的字段,之后若T2进行回滚,T1读取的内容就是临时且无效的;

不可重复读:对于两个事务 T1 和 T2 , T1 读取了一个字段,然后T2 更新了该字段,之后T1再次读取同一个字段,值就不同了;

幻读:对于两个事务 T1,T2,T1 从表中读取了一个字段,然后T2在该表中插入了一些新的行,之后T1再次读取同一个表,就会多出几行;

不可重复读 和 幻读 的区别:不可重复读针对的是更新和删除操作,幻读针对的是插入操作,比如:T1 正在操作一条记录,如果加锁,T2 就不能对这条记录进行更新和删除,这就避免了 不可重复读,但无法避免 T2 插入新的数据,也就是无法避免幻读,InnoDB 中通过 gap 锁来解决幻读问题,这里我们不讨论 gap 锁;

3、InnoDB 如何解决事务并发问题

InnoDB 为了解决事务并发导致的脏读、不可重复读、幻读问题,提供了事务隔离机制,共有四种隔离级别:

读未提交:一个事务还未提交,他的变更就能被其他事务看到,这个级别就是没有任何隔离;

读已提交:一个事务的变更,只有在提交后才能被其他事务看到;

可重复读:一个事务执行过程中看到的数据,总是跟这个事务启动时看到的数据一致,即使数据被其他事务更改并提交,也是不可见的;

串行化:对于同一行记录,写会加写锁,读会加读锁,当出现读写锁冲突时,后访问的事务必须等之前的事务执行完成才能继续执行;

隔离级别越高,数据一致性越能得到保障,但并发性也就越低,MySQL 默认的隔离级别是 可重复读;

针对隔离级别的相关操作:

-- 查看隔离级别
show variables like 'transaction_isolation';

-- 设置当前会话隔离级别
set session transaction isolation level read uncommitted;
set session transaction isolation level read committed;
set session transaction isolation level repeatable read;
set session transaction isolation level serializable;

-- 设置整个库的隔离级别
set global transaction isolation level read uncommitted;
set global transaction isolation level read committed;
set global transaction isolation level repeatable read;
set global transaction isolation level serializable;

4、InnoDB 隔离级别的实现

InnoDB 的四种隔离级别,读未提交不需要做任何操作,做任何操作都读当前最新的值就可以了,串行化是严格的互斥操作,通过加锁来实现,读已提交和可重复读则通过 MVCC 来实现,下面我们重点研究 MVCC 的实现原理;

4.1 两种读模式

  • 快照读
    读取事务快照的历史版本数据,无需加锁,正常的 select 语句就是快照读;

  • 当前读
    读取数据库最新的数据,需要加锁才能实现,采用当前读的语句:

    select ... lock in share mode;
    select ... for update;
    delete
    update
    insert into
    replace into
    

    看下面两个例子:

    /**
     * 假设 num 初始值为 1,按照 代码行中的标号顺序进行执行,
     * session1 中 ② 和 ⑦ 查询到的 num 分别是多少?
     */
    session1:
    start transaction with consistent snapshot; -- ①
    select * from test; -- ②
    select * from test; -- ⑦
    commit; -- ⑧
    
    session2:
    start transaction with consistent snapshot; -- ③
    update test set num = 2 where id = 1; -- ④
    select * from test; -- ⑤
    commit; -- ⑥
    
    /**
     * 假设 num 初始值为 1,按照 代码行中的标号顺序进行执行,
     * session1 中 ② 和 ⑧ 分别查到什么结果?⑤ 能顺利执行吗?
     */
    session1:
    start transaction with consistent snapshot; -- ①
    select * from test; -- ②
    update test set num = 3 where id = 1; -- ⑤
    select * from test; -- ⑧
    commit; -- ⑨
    
    session2:
    start transaction with consistent snapshot; -- ③
    update test set num = 2 where id = 1; -- ④
    select * from test; -- ⑥
    commit; -- ⑦
    

    以上两个例子:

    1. 由于读操作是快照读,而事务开始时已经通过 with consistent snapshot 语句生成了快照,因此两次查询结果都是 1;
    2. 第一个读是快照读,结果是 1,执行到更新语句时,由于 session2 先一步做了更新,而更新属于当前读,因此两个更新同时操作时会冲突,这里通过锁来解决冲突,从而可知,session1 的 ⑤ 在 session2 提交之前都处于阻塞等待状态,session 2 提交之后锁释放了,session1 才能继续执行,第二个查询得到的结果是更新之后的结果 3;

4.2 undo log

  • MySQL 中对于每一条更新操作,都会记录 undo log 用于回滚和支持多版本并发控制操作(MVCC);
  • undo log 记录的是逻辑日志,可以认为当delete一条记录时,undo log 中会记录一条对应的 insert 记录,反之亦然,当update 一条记录时,它记录一条对应相反的 update 记录;
  • 应用到行版本控制的时候,是这样工作的:当读取的某一行被其他事务锁定时,它可以从 undo log 中分析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取;

这里我们不做 undo log 的详细工作原理的研究,只需要知道 MVCC 需要根据 undo log 来实现多版本数据的查找就可以了;

4.3 表的隐藏表字段

  • InnoDB 里每个事务都有一个唯一的 ID,即:transaction id,它是事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的;

  • InnoDB 通过将每条数据与操作它的 transaction id 和 undo log 做关联,来达到维护多个数据版本的目的,那么如何做关联呢?就是为表增加隐藏列,InnoDB 会默认为每张表增加三个列:

    列名称 描述
    DB_ROW_ID 行标识(隐藏的自增 id,如果没有明确的聚集索引,InnoDB 会自动生成一个聚集索引,这个聚集索引的值就是该 id);
    DB_TRX_ID 插入或更新行的最后一个事务id(删除也视为更新,但会标记为已删除);
    DB_ROLL_PTR 指向对应的 undo log 用于回滚到上一个版本;
  • 一个事务对一行进行插入或更新,会将这一行的 DB_TRX_ID 的值修改为自身的事务 id,并将 DB_ROLL_PTR 指向生成的 undo log;

4.4 一致性视图

  • MySQL 中有两种视图,一种是 view,是用一个查询语句定义的虚拟表,在调用的时候调用查询语句并生成结果,另一种是 实现 MVCC 时用到的一致性视图 consistent read view,用于支持 RC(读提交)和 RR(可重复读)隔离级别的实现;
  • 一个事务以生成一致性视图的时刻为准,如果一个数据版本在其之前生成,事务就认为该数据可见,如果在其之后生成事务就认为不可见,必须通过回滚的方式找到上一个版本,如果上一个版本还在一致性视图生成之后,那就继续往前回滚,直到找到比一致性视图生成时刻更早的数据版本为止,这个“回滚”操作就是通过前面表隐藏字段记录的版本信息和 undo log 来实现的;
    事务生成的时刻:
    -- 以下事务中,第一次执行快照读操作的时候生成一致性视图
    start transaction;
    ...
    commit / rollback;
    
    -- 可以通过以下语句控制事务在启动时就生成一致性视图
    start transaction with consistent snapshot;
    
  • 在实现上,InnoDB 为每个事物构建了一个数组,用来存储这个事务启动瞬间,当前所有活跃的事务 id,所谓活跃是指已经启动但未提交,数组里最小的 id 记为低水位,当前已创建的最大的事务 id+1 记为高水位,这个数组和高水位一起组成了当前事务的一致性视图;(在分配事务 id 和生成视图之间的时间段,可能产生新的事务,其 id 大于当前事务 id,若其在当前事务的一致性视图生成之前提交了,则其结果对当前事务也是可见的
  • 可以通过低水位和高水位将 MySQL 中的事务 id 分为三段:小于低水位、大于等于低水位且小于高水位、大于等于高水位,我们将三段简称为 低段、中段、高段,判断一条数据是否对当前事务可见,直接根据这条数据的 DB_TRX_ID 与当前事务的一致性视图比较即可:
    1. 如果数据的 DB_TRX_ID 落在了低段,即:DB_TRX_ID 小于低水位,则说明这条数据在事务生成一致性视图时已提交,可见;
    2. 如果数据的 DB_TRX_ID 落在了高段,即:DB_TRX_ID 大于等于高水位,则说明这条数据在事务生成一致性视图时还未提交,不可见;
    3. 如果数据的 DB_TRX_ID 落在了中段,即:DB_TRX_ID 大于等于低水位且小于高水位,此时,若 DB_TRX_ID 在数组中,则说明生成一致性视图时该数据还未提交,不可见,否则,这条数据可见;

以上就是一致性视图的工作原理;
RR 隔离级别是在事务执行第一条快照读语句时创建一致性视图的;
RC 隔离级别则每次执行快照读语句是都会创建最新的一致性视图;

4.5 例程

执行序号 session1 session2 session3
1 start transaction;
2 start transaction;
3 insert into test(num) values(100);
4 insert into test(num) values(101);
5 select num from test;
6 insert into test(num) values(102);
7 insert into test(num) values(103);
8 insert into test(num) values(104);
9 select num from test;
10 select num from test;
11 select num from test;

最终,三个 session 中的查询语句结果分别是多少?

  1. 假设 session1 中的事务 id 为 5,则 session2 中事务 id 为 6,session3 中四个事务的 id 分别为 7、8、9、10(MySQL 中不显示指定事务时,默认一条语句为一个事务);
  2. 根据以上假设:
    • session1 中第 5 行创建一致性视图时,活跃事务队列为(5, 6),低水位为 5,高水位为 7,由于 session2 的事务 id 在队列中,因此 session1 的事务对插入的 101 不可见,因此第 5 行的查询结果为 100;
    • session3 中第 9 行创建一致性视图时,活跃事务队列为(5, 6, 10),低水位为 5,高水位为 11,它不可见前面两个事务插入的数据,因此查询的结果为 102、103、104;
    • session2 中第 10 行创建一致性视图时,活跃事务队列为(5, 6),低水位为 5,高水位为 11(此时全局最大的事务 id 为 10),它对 session3 的几个事务插入的数据均可见,但对 session1 插入的数据不可见,因此查询的结果为 101、102、103、104;
    • session1 中第 11 行,由于之前已经创建过一致性视图了,因此这里不再创建,查询结果还是 100;
    • 以上操作均在 RR 隔离级别下进行,若是 RC 隔离级别,则每次快照读都会创建最新的一致性视图,具体结果可自行推导;

5、参考资料

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