热衷学习,热衷生存!

积淀、分享、成长,让本人和别人都能有所播种!

一、一致性非锁定读

对于一致性非锁定度的实现,通常的形式是加一个版本号或者工夫戳,在更新数据的时候版本号+1或者更新工夫戳。查问时,将以后可见的版本号与对应记录的版本号做比照,如果记录的版本号小于可见版本,则示意该记录可见。

InnoDB存储引擎中,多版本控制就是对一致性非锁定读的实现。如果读取的行正在执行delete或者update操作,这时候读取操作不会去期待行开释锁,而是会去读取行的一个快照数据,对于这种读取历史数据的形式,叫做快照度。

可反复读读取已提交两个隔离级别下,如果是执行一般的select语句(不包含select ... lock in share mode, select ... for update)则会应用一致性非锁定读

并且在可反复读MVCC 实现了可反复读和避免局部幻读。

二、锁定读(以后读)

如果执行的是上面语句,就是锁定读

  • select ... lock in share mode
  • select ... for update
  • insertupdatedelete操作

在锁定读下,读取的是数据的最新版本,这种读也被称为以后读。锁定读会对读取到的记录加锁:

  • select ... lock in share mode:对记录加S锁,其余事务也能够加S锁,如果加X锁则会被阻塞。
  • select ... for updateinsertupdatedelete:对记录加X锁,且其余事务不能加任何锁。

在一致性非锁定读下,即便读取的数据曾经被其它事务加上了X锁,记录也是能够被读取的,读取的是快照数据。下面说了在可反复读隔离级别MVCC避免了局部幻读,这个局部是指在一致性锁定读状况下,只能读取到第一次查问之前插入的数据(依据Read View判断数据可见性,Read View在第一次查问时生成)。然而如果是以后读,每次读取的都是最新数据,这个如果两次查问两头有其余事物插入数据就能够产生幻读。所以InnoDB可重读时,如果以后执行的是以后读,则会对读取的记录应用Next-Key Lock,来避免其余事物在间隙间插入数据。

快照读和以后读栗子

开启A和B两个会话。

首先在A会话中查问user_id = 1user_name的记录:

begin;select user_name from t_user where user_id = 1;

查问进去的后果是:user_name = '张三'

而后再B会话对user_id = 1user_name进行批改:

update t_user set user_name = '李四' where user_id = 1;

而后再回到A会话持续做查问操作:

select user_name from t_user where user_id = 1;select user_name from t_user where user_id = 1 for update;select user_name from t_user where user_id = 1 lock in share mode;

三条数据查问进去的后果别离是:user_name = '张三'user_name = '李四'user_name = '李四'

能够看出A会话中的第一条查问是快照读,读取到的以后事务开启时的数据记录,前面两个查问是以后读,读取到的是最新数据。

三、InnoDB对MVCC的实现

MVCC(Multi-Version Concurrency Control) 多版本并发管制。

MVCC的实现次要依赖于:暗藏字段、Read Viewundo log。在外部实现中通过数据行的DB_TRX_IDRead View来判断数据的可见性,如不可见,则通过数据行的DB_ROLL_PTR找到undo log中的历史版本。每个事务读取到的数据版本可能是不统一的,在同一个事务中,用户只能看到该事务创立Read View之前曾经提交的批改或者该事务自身做的批改。

暗藏字段

在外部,InnoDB存储引擎为每行数据增加了三个暗藏字段,如下:

  • DB_TRX_ID(6字节):示意最初一次插入或者更新改行的事务id,当咱们要开始一个事务时,会向InnoDB的事务零碎申请一个事务id,这个事务id是一个严格递增且惟一的数字,以后行是被哪个事务批改的,就会把对应的事务id记录在以后行中。对于delete操作会在记录头Record header中的delete_flag字段将其标记为已删除。
  • DB_ROLL_PTR(7字节):回滚指针,这个回滚指针指向一个undo log日志的地址,能够通过undo log日志放这条记录复原到历史版本,如果该行未被更新,则为空。
  • DB_ROW_ID(6字节):行id,用来惟一标识一行数据,如果没有设置主键且该表没有惟一非空索引时,会应用该id来当主键生成聚簇索引。

Read View

class ReadView {    private:        trx_id_t m_low_limit_id;   /*大于等于这个id的事务均不可见*/        trx_id_t m_up_limit_id;    /*小于这个id的事务均可见*/        trx_id_t m_creator_trx_id; /*创立该Read View的事务id*/        trx_id_t m_low_limit_no;   /*事务Number, 小于该Number的Undo log均能够被purge*/        ids_t m_ids;               /*创立该Read View时的沉闷事务列表*/        m_closed;                   /*标记Read View是否敞开*/}

Read View次要是用来做可见性判断,外面保留了“以后对本事务不可见的其余沉闷事务”。

次要有以下字段:

  • m_low_limit_id:目前呈现过的最大事务id+1,即下一个将为调配的事务id,大于等于这个id的数据版本均不可见。
  • m_up_limit_id:沉闷事务列表m_ids中最小的事务id,如果m_ids为空,则为m_low_limit_id,小于这个id的数据版本均可见。
  • m_idsRead View创立时其余未提交沉闷事务ID列表。创立Read View时,将以后未提交事务id记录下来,后续即便他们批改了记录行的值,对于以后事务也是不可见的。m_ids不包含以后事务本人和已提交的事务。
  • m_creator_trx_id:创立该Read View的事务id。
事务可见性示意图

undo log

undo log次要有两个作用:

  • 将事务回滚时用于将数据恢复到批改前的样子。
  • 另一个作用是MVCC,当读取记录时,若该条记录被其余事务占用或者以后版本对该事务不可见时,则能够通过undo log读取之前的版本数据,实现非锁定读。

InnoDB存储引擎中undo log分为了两种:insert undo logupdate undo log

  • insert undo log:在insert 操作中产生的undo log。因为insert操作的记录只对事务自身可见,对其余事务不可见,所以insert undo log能够在事务提交后间接删除。不须要purge操作。

    insert时数据初始化状态:

  • update undo logupdate或者delete操作中产生的undo log。该undo log可能须要提供MVCC机制,因而不能在事务提交后就进行删除。提交时放入undo log链表,期待purge线程进行最初的删除。

    数据第一次被批改时:

    数据第二次被批改时:

不同事务或者雷同事务对同一记录进行的批改,会使该行记录的undo log成为一条链表,链首就是最新的记录,链尾就是最早的旧记录。

数据可见性算法

InnoDB存储引擎中,创立一个新事务后,执行每个select语句前都会创立一个快照(Read View),快照中保留了以后数据库所有正在处于沉闷的事务(没有提交)id。说简略点就是保留了不应该被以后事务所能见的其余事务id(即m_ids)。当用户在这个事务中要读取某个记录行的时候,InnoDB会将该记录行的DB_TRX_IDRead View中的以后事务事务id进行比拟,判断是否满足可见条件。

具体的比拟算法源码如下:图源

  1. 如果记录DB_TRX_ID < m_up_limit_id示意最新批改的该行事务在以后事务创立快照之前就曾经提交了,所以改行记录的值对以后事务是可见的。
  2. 如果记录DB_TRX_ID >= m_low_limit_id示意最新批改的行事务在以后事务创立快照之后再才批改该行,所以该记录行的值对以后事务是不可见的,跳到步骤5。
  3. md_ids为空,阐明在以后事务创立快照之前,批改该行的事务就曾经提交了,所以该记录行的值对所有事务都可见。
  4. 如果m_up_limit_id <= DB_TRX_ID < m_low_limit_id ,表明最新批改行的事务在以后事务创立快照时可能处于“沉闷状态”或者“已提交状态”。所以要对沉闷事务列表m_ids进行查找(源码中用的二分查找法):

    • 如果在沉闷事务列表m_ids能找到DB_TRX_ID阐明:①在以后事务创立快照时,改行记录的值被事务DB_TRX_ID的事务批改了,但没有提交;或者②在以后事务创立快照后,该记录行的值被ID为DB_TRX_ID的事务批改了,这些状况下这个记录行的值对以后事务是不可见的。跳到步骤5。
    • 如果在沉闷事务列表m_ids找不到,阐明DB_TRX_ID的事务在批改该记录行的值在以后事务创立快照前曾经提交了,所以该行记录的值对以后事务是可见的。
  5. 在该行记录的DB_ROLL_PTR执行所指向的undo log取出快照数据,用快照数据的DB_TRX_ID跳到步骤1从新开始判断直到找到满足的快照版本或返回空。

四、读取已提交(RC)和可反复(RR)隔离级别下MVCC的差别。

在事务隔离级别RCRR下,InnoDB存储引擎应用MVCC生成的Read View的机会不同。

  • RC隔离级别下每次 select查问前都生成一个Read Viewm_ids列表)。
  • RR隔离级别下只在事务开始后第一次select数据前生成一个Read Viewm_ids列表)。

五、MVCC解决不可反复读问题

尽管RCRR都通过MVCC来读取快照数据,然而因为生成Read View机会不同,从而在RR级别下实现可反复读。

举个例子:

事务101事务102事务103
T1begin;
T2begin;begin;
T3update user set name = '张三' where id = 1;
T4update user set name = '李四' where id = 1;...select * from user where id = 1;
T5commit;update uset set name = '王五' where id = 1;
T6select * from user where id = 1;
T7update uset set name = '赵六' where id = 1;
T8commit;
T9select * from user where id = 1;
T10commit;

在RC下Read View生成状况

  • 假如工夫线来到T4,那么此时数据行 id = 1的版本链为:

    因为RC级别下每次查问都会生成Read View,并且事务101、102没有提交,此时103事务生成的Read View中沉闷事务为m_ids为[101,102],m_low_limit_id为104,m_up_limit_id为101,m_creator_id为103。

    • 此时最新记录的DB_TRX_ID为101,所以m_up_limit_id <= DB_TRX_ID < m_low_limit_id,所以要在m_ids列表中查找,发现DB_TRX_ID存在列表中,所以这个记录不可见。
    • 杜绝DB_ROLL_PTR找到undo_log中上一版本记录,上一条记录的DB_TRX_ID还是101不可见。
    • 持续找上一条DB_TRX_ID为1,满足1 < m_up_limit_id所以可见,所以事务103查问的数据为name = 菜花
  • 假如工夫线来到T6,数据的版本链为:

    因为在RC级别下,从新生成Read View,此时事务101曾经提交,102事务未提交,所以此时Read View中沉闷的事务m_ids为[102],m_low_limit_id为104,m_up_limit_id为102,m_creator_id为103。

    • 此时最新记录的DB_TRX_ID为102,m_up_limit_id <= DB_TRX_ID < m_up_limit_ud,所以要在m_ids中查找,发现DB_TRX_ID存在列表中,那么这个记录不可见。
    • 依据DB_ROLL_PTR找到undo log中的上一版本记录,上一条记录的DB_TRX_ID为101,满足101 < t_up_limit_id,所以记录可见,所以在T6工夫点查问到的数据为name = 李四 ,与工夫T4查问到的后果不统一,产生了不可反复读。
  • 假如工夫先来到T9,数据的版本链为:

    • 因为在RC级别下,从新生成Read View,此时事务101、102都曾经提交,所以m_ids为空,则m_up_limit_id = m_low_limit_id = 104,最新版本事务ID为102,满足102 < m_up_limit_id,所以可见,查问后果为name = 赵六

总结:在RC隔离级别下,事务在每次查问的开始都会生成Read View,所以导致不可反复读。

在RR选Read View生成状况

在可反复读级别下,只会在事务开始后的第一次读取数据是生成一个Read View(m_ids)。

  1. 假如工夫线来到T4,那么此时数据行 id = 1的版本链为:

    在执行以后select语句时生成一个Read View,事务101,102未提交,此时m_ids为[101,102],m_low_limit_id为104,m_up_limit_id为101,m_creator_trx_id为103

    此时和RC级别下一样:

    • 最新记录的 DB_TRX_ID 为 101,m_up_limit_id <= 101 < m_low_limit_id,所以要在 m_ids 列表中查找,发现 DB_TRX_ID 存在列表中,那么这个记录不可见
    • 依据 DB_ROLL_PTR 找到 undo log 中的上一版本记录,上一条记录的 DB_TRX_ID 还是 101,不可见
    • 持续找上一条 DB_TRX_ID为 1,满足 1 < m_up_limit_id,可见,所以事务 103 查问到数据为 name = 菜花
  2. 假如工夫线来到T6,那么此时数据行 id = 1的版本链为:

    因为在RR级别下只会生成一次Read View,所以此时m_ids还是为[101,102],m_low_limit_id为104,m_up_limit_id为101,m_creator_trx_id为103

    • 最新记录DB_TRX_ID为102,满足m_up_limit_id <= 102 < m_low_limit_id,且在m_ids中存在102,所以这个记录不可见。
    • 依据BD_ROLL_PTR找到undo log中的上一版本,上一条记录的DB_TRX_ID为101,和下面一样,不可见。
    • 持续依据DB_ROLL_PTR找到undo log的中上一版本记录,上一条记录的DB_TRX_ID还是101,还是不可见。
    • 持续找上一条DB_TRX_UD为1,满足1 < m_up_limit_id,可见,所以事务103查问到的数据为name=菜花,和T4查问进去的后果一样,防止了不可反复。
  3. 假如工夫线来到T9,那么此时数据行 id = 1的版本链为:

    此时状况和T6齐全一样,因为曾经生成了 Read View,此时仍然沿用 m_ids :[101,102] ,所以查问后果仍然是 name = 菜花

总结:在RR级别下只会在事务开始后的第一次查问生成Read View,所以能够防止不可反复的景象。

六、MVCC+Next-key Lock避免幻读

InnoDB存储引擎在RR级别下通过MVCCNext-key Lock来解决幻读问题:

  1. 执行一般select,此时会以MVCC快照读的形式读取数据

    在快照读的状况下,RR隔离级别只会在事务开始后的第一次查问生成Read View,并应用至事务提交。所以在生成Read View之后其它事务所做的更新、插入记录版本对以后事务并不可见,实现了可反复读和避免快照读下的“幻读”。

  2. 执行select for update/lock int share mode、insert、update、delete等以后读

    在以后读下,读取的都是最新的数据,如果其它事务有插入新的记录,并且刚好在以后事务查问范畴内,就会产生幻读。InnoDB应用Next-key lock来避免这种状况,在执行以后读时,会锁定读取到的记录,同时也会锁定它们的间隙,避免其它事务在查问范畴内插入数据,只有我不让你插入,就不会产生幻读。

参考:https://javaguide.cn/database...