美文网首页
InnoDB 事务隔离机制

InnoDB 事务隔离机制

作者: smartmhs | 来源:发表于2019-07-23 23:54 被阅读0次

    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、参考资料

    相关文章

      网友评论

          本文标题:InnoDB 事务隔离机制

          本文链接:https://www.haomeiwen.com/subject/zeczlctx.html