视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
我所理解的MySQL之四:事务、隔离级别及MVCC
2020-11-08 22:13:17 责编:小采
文档


幻读会发生在读未提交、读已提交、可重复读的隔离级别中。

这里需要额外注意的是:幻读和不可重复读都是说在一个事务中的同一个查询语句结果不同,但幻读更侧重于查询到其他事务新插入的数据(insert)或其他事务删除的数据(delete),而不可重复读的范围更广,只要结果不同就可以认为是不可重复读,但一般我们认为不可重复读更侧重于其他事务对数据的更新(update)。

4.4 小结

通过上面的描述,我们已经知道四种隔离级别的概念以及它们分别会遇到的问题,事务的隔离级别越高,隔离性就越强,所遇到的问题也就越少。但同时,隔离级别越高,并发能力就越弱。

下表是对隔离级别的概念不同隔离级别会发生的问题情况的小结:

隔离级别脏读不可重复读幻读概念
读已提交事务中的修改,即便没有提交,对其他事务也都是可见的
读未提交
事务中的修改只有在提交之后,才会对其他事务可见
可重复读

一个事务中多次查询相同的记录,结果总是一致的
可串行化


事务都是串行执行的,读会加读锁,写会加写锁

5. MVCC

MVCC(Multi-Version Concurrency Control)即多版本并发控制,这是 MySQL 为了提高数据库并发性能而实现的。它可以在并发读写数据库时,保证不同事务的读-写操作并发执行,同时也能解决脏读、不可重复读、幻读等事务隔离问题。

在前文讨论幻读的时候提到过当前读的概念,正是由于当前读,才会在可重复读的隔离级别下也会发生幻读的情况。

在解释可重复读隔离级别下发生幻读的原因之前,首先介绍 MVCC 的实现原理。

5.1 MVCC 的实现原理

首先我们需要知道,InnoDB 的数据页中每一行的数据是有隐藏字段的:

  • DB_ROW_ID: 隐式主键,若表结构中未定义主键,InnoDB 会自动生成该字段作为表的主键
  • DB_TRX_ID: 事务ID,代表修改此行记录的最后一次事务ID
  • DB_ROLL_PTR: 回滚指针,指向此行记录的上一个版本(上一个事务ID对应的记录)
  • 每一条修改语句都会相应地记录一条回滚语句(undo log),如果把每一条回滚语句视为一条数据表中的记录,那么通过事务ID和回滚指针就可以将对同一行的修改记录看作一个链表,链表上的每一个节点就是一个快照版本,这就是 MVCC 中多版本的意思。

    举个例子,假设对 user 表中唯一的一行「刺猬」进行多次修改。

    update user set name='重塑' where id=1;update user set name='木马' where id=1;update user set name='达达' where id=1;复制代码

    那么这条记录的版本链就是:

    在这个版本链中,头结点就是当前记录的最新版本。DB_TRX_ID 事务ID 字段是非常重要的属性,先 Mark 一下。

    除此之外,在读已提交(RC,Read Committed)和可重复读(RR,Repeatable Read)的隔离级别中,事务在启动的时候会创建一个读视图(Read View),用它来记录当前系统的活跃事务信息,通过读视图来进行本事务之间的可见性判断。

    在读视图中有两个重要的属性:

  • 当前事务ID:表示生成读视图的事务的事务ID
  • 事务ID列表:表示在生成读视图时,当前系统中活跃着的事务ID列表
  • 最小事务ID:表示在生成读视图时,当前系统中活跃着的最小事务ID
  • 下一个事务ID:表示在生成读视图时,系统应该分配给下一个事务的事务ID
  • 需要注意下一个事务I的值,并不是事务ID列表中的最大值+1,而是当前系统中已存在过的事务的最大值+1。例如当前数据库中活跃的事务有(1,2),此时事务2提交,同时又开启了新事务,在生成的读视图中,下一个事务ID的值为3。

    我们通过将版本链与读视图两者结合起来,来进行并发事务间可见性的判断,判断规则如下(假设现在要判断事务A是否可以访问到事务B的修改记录):

  • 若事务B的当前事务ID小于事务A的最小事务ID的值,代表事务B是在事务A生成读视图之前就已经提交了的,所以事务B对于事务A来说是可见的。
  • 若事务B的当前事务ID大于或等于事务A下一个事务ID的值,代表事务B是在事务A生成读视图之后才开启,所以事务B对于事务A来说是不可见的。
  • 若事务B的当前事务ID在事务A的最小事务ID下一个事务ID之间(左闭右开,[最小事务ID, 下一个事务ID)),需要分两种情况讨论:
  • 若事务B的当前事务ID在事务A的事务ID列表中,代表创建事务A时事务B还是活跃的,未提交,所以事务B对于事务A来说是不可见的。
  • 若事务B的当前事务ID不在事务A的事务ID列表中,代表创建事务A时事务B已经提交,所以事务B对于事务A来说是可见的。
  • 如果事务B对于事务A来说是不可见的,就需要顺着修改记录的版本链,从回滚指针开始往前遍历,直到找到第一个对于事务A来说是可见的事务ID,或者遍历完版本链也未找到(表示这条记录对事务A不可见)。

    这就是 MVCC 的实现原理。

    5.2 读视图的创建时机

    这里需要注意的是读视图的创建时机,在上面的论述中我们已经知道事务在启动时会创建一个读视图(Read View),而开启一个事务有两种方式,一是 begin/start transaction,二是start transaction with consistent snapshot,通过这两种方式开启事务,创建读视图的时机也是不同的:

  • 如果是以 begin/start transaction 方式开启事务,读视图会在执行第一个快照读语句时创建
  • 如果以 start transaction with consistent snapshot 方式开启事务,同时便会创建读视图
  • 5.3 MVCC 的运行过程

    为了详细说明 MVCC 的运行过程,下面举个例子,假设当前存在有两个事务(事务隔离级别为 MySQL 默认的可重复读):

    这里需要注意的是事务的启动时机,在上面的论述中我们已经知道事务在启动时会创建一个读视图(Read View),而开启一个事务有两种方式,一是 begin/start transaction,二是start transaction with consistent snapshot,通过这两种方式开启事务,创建读视图的时机也是不同的:

  • 如果是以 begin/start transaction 方式开启事务,读视图会在执行第一个快照读语句时创建
  • 如果以 start transaction with consistent snapshot 方式开启事务,同时便会创建读视图
  • 时刻事务A事务B
    1start transaction with consistent snapshot;
    2
    start transaction with consistent snapshot;
    3
    update user set name='重塑' where id=1;
    4select name from user where id=1;(N1)
    5
    commit;
    6select name from user where id=1;(N2)
    7commit;

    然后根据上面所描述的版本链以及两个事务开启时的读视图来分析 MVCC 的运行过程。

    上图是两个事务开启时的读视图,而当事务B的更新语句执行之后,id=1行的版本链如下所示。

    先来看N1处的查询语句,事务B的当前事务ID=2,其值等于事务A的下一个事务ID,所以按照上文中所论述的可见性判断,事务B对于事务A来说是不可见的,需要循着当前行的版本链网上检索。

    于是循着版本链来到DB_TRX_ID=1事务ID=1的历史版本,恰巧等于事务A的事务ID值,也就是事务A开启时该行的版本,此版本对于事务A来说当然是可见的,所以读取到了id=1行的name='刺猬',即最终N1=刺猬。

    再来看N2处的查询语句,此时事务B已提交,版本链还是如上图所示,由于当前版本的事务ID等于事务A读视图中的下一个事务ID,所以当前版本的记录对于事务A来说是不可见的,所以同样N2=刺猬。

    这里需要注意的是,若例子中事务A的时刻4语句变更为对该行的更新语句,那么事务A便会等待事务B提交之后再执行更新语句,这是因为事务B未提交,即事务B对于id=1行的写锁未释放,而事务A也要更新该行,必须是更新当前的最新版本(当前读)才可以,所以事务A就被阻塞了,必须等待事务B对该行的写锁释放,才会继续执行更新语句。

    5.4 RC 与 RR 生成读视图的时机对比

    上面所讨论的 MVCC 运行过程都是针对可重复读(RR, Repeatable Read)隔离级别的,如果是读已提交(RC, Read Committed)级别呢?

    上文中已经讨论过读已提交隔离级别中关于不可重复读的情况了,这里就不再举例,直接给出结论就可以了。

  • 可重复读(RR, Repeatable Read)隔离级别下生成读视图(Read View)的时机是开启事务的时候
  • 读已提交(RC, Read Committed)隔离级别下生成读视图(Read View)的时机是每一条语句执行前
  • 对于上文中描述 MVCC 执行过程中的例子,如果隔离级别是读已提交(RC, Read Committed):

  • N1处的查询语句,由于事务B还未提交,事务A可见的版本依旧是事务ID=1的版本,所以N1=刺猬
  • N2处的查询语句,事务B已提交,N2处查询语句执行时也会生成读视图,其当前事务ID=3,而在该记录的版本链中,当前版本的事务ID DB_TRX_ID=2,在N2查询语句事务ID之前,是可见的,所以N2=重塑
  • 5.5 当前读与快照读

  • 当前读:读取记录的最新版本
  • 快照读:读取记录时会根据一定规则读取事务可见版本的记录
  • 5.6 可重复读发生幻读的原因

    在理解了 MVCC 之后,我们再来看在可重复读隔离级别下发生幻读的原因。上文中说到正是由于当前读,才会在可重复读的隔离级别下发生幻读的情况,首先来回顾一下例子。

    时刻事务A事务B
    1begin;
    2select name from user;(N1)
    3
    begin;
    4
    insert into user values(2, '五条人');
    5
    commit;
    6select name from user;(N2)
    7select name from user for update;(N3)
    8commit;

    N1,N2处的查询想必已经十分明确都是「刺猬」了。而在N3处所使用的查询语句是for update,使用它进行查询就会对目标记录添加一把「行级锁」,行级锁的意义以后再说,现在只需要知道for update能够锁住目标记录就可以了。

    加锁自然是防止别人修改,那么理所当然,锁住的当然也就是记录的最新版本了。所以,在使用for update进行查询的时候,会使用当前读,读到目标记录的最新版本,所以在N3处的查询语句就会把事务B中本对于事务A来说不可见的记录也查询出来,也就发生了幻读。

    使用当前读的语句有:

  • select ... for update
  • select ... lock in share mode(共享读锁)
  • update ...
  • insert ...
  • delete ...
  • 6. 温故知新

    1. 事务的ACID四大特性
    2. 自动提交与隐式提交(隐式提交的语句)
    3. 事务的隔离级别
    4. 事务各个隔离级别可能出现的问题
    5. MVCC 的实现原理与过程
    6. RC 与 RR 生成读视图的时机

    更多相关免费学习推荐:mysql教程(视频)

    下载本文
    显示全文
    专题