美文网首页MySQL纵横研究院数据库技术专题社区 程序员技术栈
【原创】MySQL InnoDB存储引擎事务隔离性的实现

【原创】MySQL InnoDB存储引擎事务隔离性的实现

作者: 正在加载更多 | 来源:发表于2019-05-25 22:01 被阅读46次

    事务

    事务保证一组数据库操作要么成功要么失败
    当数据库中有多个事务同时进行时,可能会出现脏读(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:如果你的答案不是这个,你可能需要再看一遍文章

    相关文章

      网友评论

        本文标题:【原创】MySQL InnoDB存储引擎事务隔离性的实现

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