MVCC (Multiversion Concurrency Control),即多版本并发控制技术,它使得大部分支持行锁的事务引擎,不再单纯的使用行锁来进行数据库的并发控制,取而代之的是,把数据库的行锁与行的多个版本结合起来,只需要很小的开销,就可以实现非锁定读,从而大大提高数据库系统的并发性能.
解决问题:
- 读写不冲突,提升读写并发数
- 是隔离级别 RR 和RC实现隔离性的手段
实现原理:
- 隐式字段中的DB_TRX_ID(事务ID)
- 隐式字段中的DB_ROLL_PTR(回滚指针)
- 隐式字段中的删除flag
- 隐式字段中的删除flag的事物版本
- undolog
- readview
mvcc 流程:
- 当更新时会将更新行数据copy一份放入undolog,且更新行的DB_TRX_ID(事务ID)递增+1,DB_ROLL_PTR(回滚指针)指向undolog
- 当删除时会将隐式字段中的删除flag设置为删除
- 查询时要根据事务ID查询可看到的记录版本,比如RR事务级别,查询时可看到版本号小于当前事物版本号的记录(如删除flag为true,要判断删除事务版本与当前查询事物版本的大小)
purge线程:
事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要;
所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除。
purge线程原理:purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view),所以purge能看到被标记delete flag的记录直接删除是不会影响其他事物的。
Read View(读视图)
核心作用: 当前事务能够看到哪个版本的数据,因为开启事务A时,系统中可能运行了很多事物,事物A能看到这些事物的哪些版本呢? 这就是由READ VIEW视图快照记录了下来。
什么是Read View,说白了Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
所以我们知道 Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。
Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的DB_TRX_ID(即当前事务ID)取出来,与系统当前其他活跃事务的ID去对比(由Read View维护),如果DB_TRX_ID跟Read View的属性做了某些比较,不符合可见性,那就通过DB_ROLL_PTR回滚指针去取出Undo Log中的DB_TRX_ID再比较,即遍历链表的DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的DB_TRX_ID, 那么这个DB_TRX_ID所在的旧记录就是当前事务能看见的最新老版本
READ VIEW中的 up_limit_id,low_limit_id :
MVCC Multi-version Concurrency Control
- 怎么个MultiVersion法?
- 每个开启的事务,会有一个tx_id,是不断自增长的,后面开的事务比前面开的事务大
- 在默认的隔离级别:RepeatableRead,可重复读,需要做成,在事务中,看不到别人的修改,怎么做到呢?
- 先引入一个概念:ReadView。ReadView里包含:
- 当前活跃的事务id列表
- 创建者的事务id
- up_limit_id , low_limit_id 上下限,小于up_limit_id 可见, 大于low_limit_id 不可见。
- 那么这个两个值是怎么赋值的呢。
- low_limit_id 是系统最大的事务id值。up_limit_id是活跃事务列表的最小事务id
- /* No future transactions should be visible in the view */
- 349 view->low_limit_no = trx_sys->max_trx_id;
- 350 view->low_limit_id = view->low_limit_no;
- /* The last active transaction has the smallest id: */
- view->up_limit_id = view->trx_ids[view->n_trx_ids - 1];
- 这样理解Read View :保存了不应该让这个事务看到的其他的事务 ID 列表
- 再引入关于可见性的规则
- 读某一行数据的时候,如果发现他的事务id < up_limit_id (活跃id列表最小值),可见。
- 如果发现数据的事务id>low_limit_id(ReadView时系统中最大的事务id) ,不可见。
- 如果发现数据的事务id在列表范围内
- 如果是id集合中的,不可见
- 如果不在id集合中,可见
- 根据上面的规则,以RepeatableRead举个例子吧,要求读一个值,一直读都是同一个值。
- 这种隔离级别下,开启事务的时候开启一个ReadView,在整个事务过程中都用这个ReadView。
- 我开启是事务是10,ReadView是 (4,8, 10),【我自己也活跃】,up_limit_id=11,low_limit_id=4
- 如果我读到一个数据的事务id是1,小于活跃列表的最小值,可见。
- 为什么?事务开启的时候生成的ReadView,除了4,8,10,其他都已经提交了,所以其实版本<4,以及5、6、7、9,都是肯定在我开启的时候已经提交了。这些版本的的数据,再怎么读都不会变。放心读吧。
- 如果我读到一个数据的事务id是12,说明他在我创建ReadView之后提交的,我不应该看见这个值,应该去undolog里找这个数据的前面的版本,如果找到<4,或者5、6、7、9都是安全的,可以读。
- 如果我读到一个数据事务id在活跃列表的范围内
- 如果就是活跃的事务之一,比如说是8,说明这个数据在我开启事务之后提交了,这个我不能看见,应该去undo log中找上一个版本来读,假设说是7。
- 7也是在这个活跃范围里,但是并不是活跃事务之一,这个版本是在开启事务之前提交的,这个我可见。
- 再举个读已提交的例子。
- 这个隔离级别是每次读都采用新的ReadView。
- 开启的事务是10。
- 读一个数据,事务id是9,ReadView活跃id是(4,8,10),按照规则,可见。
- 过一会再读这个数据,发现事务id变成了11,活跃的id还是(4,8,10),但是因为开启了新的ReadView,当前系统最大事务id>11(因为我们已经读到11了嘛),判断规则是:不在活跃id列表,所以可见。这回就读到了这个数据的新版本了
- 先引入一个概念:ReadView。ReadView里包含:
参考
https://www.jianshu.com/p/8845ddca3b23
https://blog.youkuaiyun.com/waltonhuang/article/details/105850745