mysql的mvcc 2021-03-22(未允禁转)

1.mvcc概要

指Multi-Version Concurrency Control,多版本并发控制。

  • 设计目的:解决读-写并发问题。注意是读-写并发,因为一般来说,一个数据如果正被写入,写操作是比较耗时的,那么此时读操作是需要阻塞的,得等这个写入结束了才能读。mvcc就是用来实现写数据的同时能够并发读的目的。但写-写仍然是互斥的。
  • 实现机制:从本质上讲,对【一份数据】做到读写并发肯定是不可能的,既然一份数据搞不定,那就用多份数据去做。数据只有一份,而且正在被修改,这个时候来一个读请求,你让它怎么读?读这个状态不稳定随时会变化的数据?no。既然一份数据做不到读-写并发,那搞几个数据副本嘛。不管读还是写,都在“自己可见的数据副本”上操作,互不影响。这里的副本就是Multi-Version
  • mvcc工作的事务隔离级别:read committed, read repearable

2.mvcc的具体实现

mvcc主要依赖隐藏字段,ReadView和undo log实现

2.1 隐藏字段

隐藏字段的主要功能是,记录最后一次修改数据的事务,并且可以追溯历史版本的数据

Internally, InnoDB adds three fields to each row stored in the database:

  • A 6-byte DB_TRX_ID field indicates the transaction identifier for the last transaction that inserted or updated the row. Also, a deletion is treated internally as an update where a special bit in the row is set to mark it as deleted.

  • A 7-byte DB_ROLL_PTR field called the roll pointer. The roll pointer points to an undo log record written to the rollback segment. If the row was updated, the undo log record contains the information necessary to rebuild the content of the row before it was updated.

  • A 6-byte DB_ROW_ID field contains a row ID that increases monotonically as new rows are inserted. If InnoDB generates a clustered index automatically, the index contains row ID values. Otherwise, the DB_ROW_ID column does not appear in any index.

以上介绍来源于MySQL8.0 InnoDB Multi-Versioning
这说明,MySQL8.0里面对每行数据加入的隐藏字段有3个:

  • 最后写事务的ID: DB_TRX_ID
  • 指向历史数据的回滚指针: DB_ROLL_PTR
  • 数据行号: DB_ROW_ID
    其中,比较重要的是DB_TRX_ID和DB_ROLL_PTR

2.2 ReadView

2.2.1 ReadView基本概念

ReadView,读视图,决定了哪些版本的数据对事务可见。换句话说,事务是依据ReadView选择可见数据的,ReadView看得见什么,事务就看得见什么。ReadView是不同事务间实现多版本数据的根本,我们讨论哪些数据对特定事务可见,其实是在讨论哪些数据对特定的ReadView可见

2.2.2 ReadView的产生

注意:事务begin不生成ReadView,一个事务的ReadView在事务内【执行普通select】操作时生成。而且,只有普通select语句才会创建ReadView,select ... lock in share mode,select ... for update不会,update、delete、insert语句也不会,因为它们都是当前读,会对访问的数据加锁
以多事务的update为例:


其中,// number表示命令的执行顺序。可以看到,// 2的update给数据上锁之后,// 3就会一直等待

读提交和可重复读,实际上就是通过ReadView的不同机制实现的。

  • 在读提交隔离级别中,事务内每次普通select都即时产生一个ReadView,因此可以读到别的事务已经提交的数据;
  • 而在可重复读隔离级别中,事务的首次普通select产生一个ReadView,并且这个ReadView用于事务内后续所有的普通select,从而实现可重复读

2.2.3 ReadView的数据结构

下面来看一下ReadView究竟长什么样

ReadView主要依赖以下几个字段:

  • creator_trx_id 当前创建的事务ID,全局递增。现在设当前创建的事务为T,首次select产生的ReadView为rv
  • up_limit_id 在事务T产生rv的那一时刻,距离事务T的【最远】【活跃】事务ID。最远即事务编号最小。
  • low_limit_id 在事务T产生rv的那一时刻,【全局】事务范围内【下一个】将被分配的事务ID,也就是未被分配的最小事务编号。例如,此刻全局的所有事务已经编号到999了,那么1000就作为rv的low_limit_id。
  • trx_ids。up_limit_id和low_limit_id是左闭右开的关系,即[up_limit_id, low_limit_id),我们把这个区间内的除事务T自己外的活动事务记为trx_ids,表示事务T的rv视图不可见的其他活跃事务。

ReadView的核心是trx_ids。

遍历所有record,记遍历到的当前record的DB_TRX_ID为t_id

    1. if t_id out of [up_limit_id, low_limit_id)
      要么是id < up_limit_id,即修改当前record的事务已经commit,显然已完成事务的数据更改是对rv可见的;
      要么是id >= low_limit_id,即在rv产生后才创建的事务,这些事务对record的修改在rv的未来发生,对rv不可见,那么就顺着record的DB_ROLL_PTR一直找到rv可见的历史版本数据
    1. else up_limit_id <= t_id < low_limit_id
      此时如果t_id in trx_ids,就表明最后修改该record的事务在rv创建时处于活跃状态中,也就是说t_id和T在rv创建时刻是并行活跃的,出于事务间的独立性,该record对rv不可见,那么就顺着record的DB_ROLL_PTR一直找到rv可见的历史版本数据
      否则,t_id not in trx_ids,说明在rv产生的时候,最后修改该record的事务早已经提交了,因此record对rv可见

2.3 Undo log

Undo log以链表形式存储了历史版本数据,如果当前记录行对事务不可见,需要顺着undo log链找到满足其可见性条件的记录行版本

3. 例子

基于ReadView的MySQL mvcc例子

本图例来自MySQL中MVCC的正确打开方式(源码佐证),本文整理的内容也参考了其中的内容。还是要多看官方文档,多看源码,比啥都强

--
补充:
个人理解,mvcc最朴素的思想还是【一写多读】,写操作只有一个版本,而读操作可以有多个版本。类似针对数据的“主写从读”思想,一主(最新版本数据,用于写)多从(多份历史版本数据,用于读)

你可能感兴趣的:(mysql的mvcc 2021-03-22(未允禁转))