zoukankan      html  css  js  c++  java
  • MySql MVCC是如何实现的-MVCC多版本并发控制?

    什么是MVCC?

    MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读

    什么是当前读和快照读?

    当前读是指读取的永远是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读
    快照读是指事务在启动的时候就“拍了一个快照”,快照的实现是基于每条数据都存在多个版本。快照读读到的不一定是最新的版本,可能会读到历史版本。不加锁的select操作就是快照读

    MVCC的实现原理

    隐式字段

    每行记录除了我们自定义的字段外,还有数据库隐式定义的字段:

    • DB_TRX_ID
      6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务
    • DB_ROLL_PTR
      7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
    • DB_ROW_ID
      6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
    • 实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了

    undo log

    InnoDB里边每行数据都会存在多个版本。每次事务更新数据的时候,都会生成一个新的数据版本,并且新的数据版本可以通过DB_ROLL_PTR找到旧的数据版本。
    如图所示:就是一个记录被多个事务连续更新后的状态。

    图中的黄色区域就是undo log;图中展示了同一行数据的5个版本,当前最新的版本为V5,value=5,它是被transaction id为10的事务更新的。

    V1~V4并不是物理上真是存在的,而是每次需要的时候根据当前版本和undo log计算出来的。

    read view

    按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。

    因此,一个事务只需要在启动的时候声明说,“以我启动的时刻为准,如果一个数据版本是在我启动之前生成的,就认;如果是我启动以后才生成的,我就不认,我必须要找到它的上一个版本”。

    当然,如果“上一个版本”也不可见,那就得继续往前找。还有,如果是这个事务自己更新的数据,它自己还是要认的。

    在实现上, InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务 ID。“活跃”指的就是,启动了但还没提交。数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。

    这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。而数据版本的可见性规则,就是基于数据的 db_trx_id 和这个一致性视图的对比结果得到的。

    MVCC实现的整体流程

    1.理解begin/start transcation和start transaction with consistent snapshot的区别

    为什么要先理解这两个命令的区别呢?因为后面我们的演示用例会用到。

    事务A 事务B
    start transacton
    update keyvalue set value=2 where key=1;
    select * from keyvalue;
    • 根据上一篇的结论,MySql默认的事务隔离级别为可重复读,autocommit=1,然后按照上图中的步骤依次执行,从结果可以看出来start transaction命令并没有真正的开启一个事务,因为事务B执行更新之后,事务A直接查到了事务B的更新,按照事务隔离级别为可重复读,开启事务A之后,事务A应该是读不到事务B的更新的。

    猜想:start transaction命令并不是一个事务的起点,在执行到它之后的第一个操作InnoDB表的语句,事务才真正的启动。
    按照这个猜想继续往下执行4和5,果然,这一次事务B的更新,在事务A中查不到了

    那么,如果想要马上启动一个事务,可以使用start transaction with consistent snapshot命令

    2.db_trx_id 和一致性视图的对比过程
    事务A 事务B 事务C
    start transaction with consistent snapshot;
    start transaction with consistent snapshot;
    update keyvalue set value=value+1 where key=1;
    update keyvalue set value=value+1 where key=1;
    select value from keyvalue where key=1;
    select value from keyvalue where key=1;
    commit;

    假设在执行这三个事务之前,key=1对应的value=1,分析事物A的语句返回的结果是什么?为什么?
    这里我们不妨假设:
    1.事务A开始前,系统里边只有一个活跃的事务ID是99;
    2.事务A,B,C的版本号分别是100,101,102,且当前系统中只有这四个事务;
    3.三个事务开始前,(1,1)这行数据的db_trx_id是90

    这样事务A的视图数组就是[99,100],事务B的视图数组是[99,100,101],事务C的视图数组是[99,100,101,102]

    从图中可以看到,第一个有效更新的事务C,把数据从(1,1)改成(1,2)。这时候,这个数据的最新版本的db_trx_id是102,而90这个版本已经成为了历史版本

    第二个有效更新是事务B,把数据从(1,2)变成了(1,3)。这时候,这个数据的最新版本的db_trx_id是101,而102又成为了历史版本。

    现在事务A要来读数据了,它的视图数组是[99,100]。

    • 找到(1,3),判断db_trx_id=101,大于100,不可见
    • 接着,找到上一个历史版本,一看db_trx_id=102,大于100,不可见
    • 再往前找,终于找到了(1,1),它的db_trx_id=90,小于99,可见

    这样执行下来,虽然期间这一行数据被修改过,但是事务A不论在什么时候查询,看到的这行数据的结果,都是一致的,所以我们称之为一致性读。

    3.更新逻辑与当前读

    细心的同学可能会有疑问:
    事务 B 的 update 语句,如果按照一致性读,好像结果不对哦?
    事务B的视图数组是先生成的,之后事务C才提交,不是应该看不见(1,2)吗?怎么能算出来(1,3)来?

    是的,如果事务 B 在更新之前查询一次数据,这个查询返回的value的值确实是 1。

    但是,当它要去更新数据的时候,就不能再在历史版本上更新了,否则事务C的更新就丢失了。因此,事务B此时的set value=value+1 是在(1,2)的基础上进行的操作。

    所以,这里就用到了这样一条规则:更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)

    因此,在更新的时候,当前读拿到的数据是(1,2),更新后生成了新版本的数据(1,3),这个新版本的 db_trx_id是101。所以,在执行事务B查询语句的时候,一看自己的版本号是101,最新数据的版本号也是101,是自己的更新,可以直接使用,所以查询得到的value的值是 3。

    这里我们提到了一个概念,叫作当前读。

    其实,除了update语句外,select语句如果加锁,也是当前读。所以,如果把事务A的查询语句select value from keyvalue where key=1;修改一下,加上lock in share mode 或 for update,也都可以读到版本号是 101 的数据,返回的value的值是3。下面这两个 select语句,就是分别加了读锁(S 锁,共享锁)和写锁(X 锁,排他锁)。

    select `value` from keyvalue where `key`=1 lock in share mode;
    select `value` from keyvalue where `key`=1 for update;
    

    特别感谢:
    http://gk.link/a/10w98
    https://www.jianshu.com/p/8845ddca3b23

    牛人之所以是牛人,是因为你现在在踩的坑,他曾经都已经踩过了。
  • 相关阅读:
    CSS盒子模型
    getContextPath、getServletPath、getRequestURI、request.getRealPath的区别
    MYSQL中的CASE WHEN END AS
    单点登录的精华总结
    git&github
    June 21st 2017 Week 25th Wednesday
    June 20th 2017 Week 25th Tuesday
    June 19th 2017 Week 25th Monday
    June 18th 2017 Week 25th Sunday
    June 17th 2017 Week 24th Saturday
  • 原文地址:https://www.cnblogs.com/dwBurning/p/mvcc.html
Copyright © 2011-2022 走看看