首页 > 技术文章 > MVCC

yangyongjie 2021-04-11 18:13 原文

 什么是MVCC?

  MVCC(Multiy Version Concurrent Control),即多版本并发控制,是一种乐观锁的实现。

  MVCC使得读不会加锁,提高了数据库的并发处理能力。通过MVCC,MySQL可以实现【读已提交】和【可重复读】等隔离级别,保证了隔离性。

 

 

 MVCC原理:

  数据库中同时存在多个版本的数据,即同一条记录数据临时保存多个版本的一种方式, 是通过undo log来实现的。

  不同的事务可以读取到不同版本的数据,从而解决脏读和不可重复读的问题

  即在某个事务对其进行操作的时候,需要查看这一条记录的隐藏列事务版本id,比对事务id并根据事务隔离级别去判断读取哪个版本的数据

  即根据事务开始的时间不同,不同的事务对同一张表,同一时刻看到的数据可能是不一样的。但是对每个事务来说,在它的执行期间,不管它执行多长时间,它看到的数据都是一致的。

  

 

 InnDB中的MVCC:

  1、事务版本号

    InnoDB中每个事务都有一个唯一的事务ID,即为transaction_id。

    事务每次开启前,都会从数据库引擎申请一个自增长的事务ID(按照时间先后严格递增),可以通过事务ID判断事务的执行先后顺序。

 

  2、隐藏字段

    对于InnoDB存储引擎,每一行记录都有两个隐藏列trx_id、roll_pointer,如果表中没有主键和非NULL唯一键时,则还会有第三个隐藏的主键列row_id

列名是否必须描述
row_id 单调递增的行ID,不是必需的,占用6个字节。
trx_id 记录操作该数据事务的事务ID
roll_pointer 这个隐藏列就相当于一个指针,指向回滚段的undo日志

    每次事务更新数据就会生成一个新的数据版本,并把transaction_id记为 trx_id,同时,旧的数据版本会保留在undo log中,而且新的版本会记录旧版本的回滚指针,通过它直接拿到上一个版本。

    所以,InnoDB中的MVCC其实是通过在每行记录后面保留两个隐藏的列来实现的。一列是事务ID:trx_id ; 另一列是回滚指针:roll_pointer 。

 

 

  3、undo log :回滚日志

    每行数据有多个版本,是依赖undo log 来实现的

    undo log记录事务修改之前的数据的一个版本,在表记录被修改之前,会先把数据拷贝到undo log里,可以用于回滚。同时提供MVCC下的快照读。

    因此undo log的作用是:

      1、事务回滚时,保证原子性和一致性

      2、用于MVCC快照读

    根据操作的不同,undo log分为两种:insert undo log 和 update undo log。

    1) insert undo log 

    insert 操作产生的undo log,因为insert 操作记录没有历史版本只对当前事务本身可见,对于其他事务此记录不可见。所以insert undo log可以在事务提交后直接删除而不需要进行purge操作。

    purge:主要任务是将数据库中已经 mark del 的数据删除,另外也会批量回收 undo pages。

 

    2)update undo log

    update和delete删除产生的undo log都属于同一类型,update可以视为insert 新数据到原位置,delete旧数据,undo log 暂时保留旧数据。

    多个事务都更新某条记录的时候,都会把旧的记录数据保存到undo log日志中。因此undo log日志中会存在同一条数据的多条记录的的情况。这也就是同一条记录在数据库中存在多个版本,也就是MVCC。

 

 

  4、版本链

    多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本,然后通过回滚指针(roll_pointer),连成一个链表,这个链表就称为版本链。

    如:一个事务对表中的一条记录(trx_id=100)进行修改,那么操作流程如下:

    1、首先获取一个事务ID,如101

    2、把表中的修改前的记录,拷贝到undo log中

    3、再修改表中的记录,

    4、把修改后记录的隐藏字段trx_id改为当前事务版本号101,并把roll_pointer指向undo log数据地址

  

 

  5、快照读和当前读

    事务的更新操作,是先读后写,这个读是查询的数据行的最新数据。而在事务中的查询语句的读,是从历史版本中查询的,叫做快照读。

    1、快照读

      查询当前事务可见的版本中的记录数据

      不加锁的普通的select语句都是快照读。

    2、当前读

      读取记录数据的最新版本

      事务对记录更新操作,读取到的是表中数据的最新值,然后执行更新操作,因为不可能去更新历史版本中的数据,可能已有其他事务先更新了。

      select当前读

      除了查询语句外,select语句如果加锁也是当前读。

      加锁方式如:lock in mode 或 for undate

      如:

        select age from t where id=2 lock in mode.

        select age from t where id=2 for update.

 

  6、read-view(视图)

    read-view是InnoDB在实现MVCC时用到的一致性视图,用于支持【读已提交】以及 【可重复读】隔离级别的实现。

    read-view 不是真实存在的,只是一个概念,undo log才是它的体现。它主要是通过版本和undo log计算出来的,作用是决定事务能看到哪些数据。

    读已提交和可重复读生成read-view的时机不同

      读已提交:每次读取数据前都生成一个read-view

      可重复读:在第一次读取时生成一个read-view,后面查询不会创建新的read-view

 

    数据版本的可见性规则:

    read-view 中主要包含当前系统中还有哪些活跃(未提交)的读写事务ID,它的数据结构是一个数组。

    在实现上InnoDB为每个事务构造了一个read-view数组,用来保存事务(读取数据前:读已提交;第一次读取数据前:可重复读)当前正在活跃(还未提交)的读写事务ID 

    

    说明:

      min_limit_id:在生成read-view时,当前系统中活跃的读写事务中最小的事务ID

      creator_trx_id:创建当前read-view的事务ID

      max_limit_id:在生成read-view时,系统中应该分配给下一个事务的ID值

      m_ids:当前系统中活跃(未提交)的事务IDS数组

         

    每个事务或语句有自己的一致性视图,普通查询语句是一致性读,一致性读会根据row trx_id和一致性视图确定数据版本的可见性。

     规则如下:

      ①:如果数据记录的事务ID,trx_id < min_limit_id,表明生成该记录版本的事务在生成当前read-view前已经提交了,所以该版本可以被当前事务访问。

      ②:如果trx_id > max_limit_id,表明生成该记录版本的事务在生成当前read-view后才创建,所以该版本不可以被当前事务访问。

      ③:如果 min_limit_id =<trx_id< max_limit_id

        (1):如果m_ids包含trx_id,则代表当前read-view生成时,这个事务还未提交,但是如果数据的trx_id=creator_trx_id的话,表明数据是自己生成的,因此是可见的,如果trx_id不等于creator_trx_id,则不可以被当前事务访问

        (2):如果m_ids不包含trx_id,则表明这个事务在read-view生成之前就已经提交了,修改的结果,当前事务是能看见的(在读已提交隔离级别中才有这种情况)

  

    问题:当一个新的事务开启后,一个比当前事务id小的事务进行了数据修改提交,那么,当前事务可以读取到比自己事务id小的事务的修改后的最新数据吗?

    答案:可以

 

  总结:查询一条记录,基于MVCC的流程

    1、生成当前事务,并获取当前事务ID

    2、生成read-view

    3、查询得到的数据,然后和read-view中的事务版本号进行比较

    4、如果不符合read-view的可见性规则,就需要从undo log中获取历史快照数据,同样进行可见性规则校验

    5、最后返回符合规则的数据

    因此,InnoDB实现MVCC,是通过read-view + undo log实现的,undo log中保存了历史数据快照,read-view的可见性规则帮助判断当前版本的数据是否可见

 

 

 什么是幻读?

  指的是当某个事务在读取某个范围内的记录时,会产生幻行,即两次读取的数据行不一致。

  原因是,一个事务先读取了某个范围的数量,同时,另一个事务新增了这个范围的数据,再次读取发现两次读取的结果不一致。

 

  MVCC使用快照读解决了部分幻读问题,但是在修改时还会存在幻读问题(如,当前事务在对记录进行范围修改时,这时其他事务在范围内插入了一条记录,根据read-view可见性规则,这条记录对当前事务是不可见的。但是这条新记录id在当前事务修改的范围内,由于修改走的是当前读,因此在修改后,这条新记录又会对当前事务可见,当前事务再次范围查询时就会出现幻读),幻读最终是通过间隙锁解决的。

 

 

 什么是间隙锁?

  间隙锁是可重复读级别下才会有的锁,结合MVCC和间隙锁可以解决幻读问题。

  间隙锁(GAP LOCK):当我们使用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁,对于键值在条件范围内但并不存在的记录,叫做间隙(GAP),InnoDB也会对这个间隙加锁,这种锁机制叫做间隙锁。

    如:user表中存在id为1,3,4,5,6的5条记录,那么这个SQL:

    update user set score=’x‘ where id<6; 

    这条sql使用的是范围作为条件,InnoDB分为会为这条sql加排他锁。不仅对符合条件的记录加了锁,对不存在的记录,id为2的间隙也会加锁。这时候如果插入一条id为2的记录就会阻塞。这就是间隙锁。

 

 

 

END.

推荐阅读