暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

MVCC多版本控制

程序媛的梦想 2021-06-04
627

一、引言

数据库并发场景有三种,分别为:

  1. 读—读:不存在任何问题,也不需要并发控制;

  2. 读—写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读、不可重复读、幻读;

  3. 写—写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失。

二、数据库如何解决并发问题?

我们知道数据库通过隔离级别实现并发事务的隔离。

数据库的隔离级别:
1、未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据;

2、提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不可重复读);

3、可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻读,但是innoDB解决了幻读;

4、串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞。

三、MySQL是如何解决这些问题的呢?

具体问题:

  1. MySQL是怎么实现可重复读的?

  2. MySQL是怎么解决幻读的?

这就用到我们的MVCC了。

四、MVCC

1. MVCC是什么?

MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。

2. MVCC有什么特点?

  1. 只适用于在read-commited和repeatable read这两种隔离级别;

  2. 只有在MySQL的innoDB引擎下才会使用MVCC;

  3. MVCC是为了实现读不加锁,读写不冲突,是一种乐观锁。

3. MVCC的好处:
  1. 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能;

  2. 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题。

4. 实现原理

1. 在学习MVCC原理之前,我们必须先了解一下,什么是MySQL InnoDB下的快照读和当前读。

快照读
像不加锁的select操作就是快照读,即不加锁的非阻塞读,它避免了加锁操作,降低了开销。但快照读可能读到的不一定是数据的最新版本,有可能是历史版本。
(快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读)。

当前读
读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。

2. 实现原理主要是依赖记录中的:3个隐式字段、undo日志、Read View 来实现的: 
(1) 3个隐式字段
DB_TRX_ID:事务ID;
DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本,用于配合undo日志,指向上一个旧版。
DB_ROW_ID:隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引。

(2) undo日志: 存历史版本的数据;

(3) Read View: 把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。

3. 流程:

每当一个事务开始的时候,InnoDB都会为事务分配一个递增的版本号,也就是事务ID。

(1) 假设有个事务插入persion表插入了一条新记录,记录如下,name为Jerry, age为24岁,隐式主键是1,事务ID和回滚指针,我们假设为NULL

(2) 现在来了一个事务1对该记录的name做出了修改,改为Tom

  • 在事务1修改该行(记录)数据时,数据库会先对该行加排他锁;

  • 然后把该行数据拷贝到undo log中,作为旧记录,既在undo log中有当前行的拷贝副本;

  • 拷贝完毕后,修改该行name为Tom,并且修改隐藏字段的事务ID为当前事务1的ID, 我们默认从1开始,之后递增,回滚指针指向拷贝到undo log的副本记录,既表示我的上一个版本就是它;

  • 事务提交后,释放锁。

(3) 又来了个事务2修改person表的同一个记录,将age修改为30岁

  • 在事务2修改该行数据时,数据库也先为该行加锁;

  • 然后把该行数据拷贝到undo log中,作为旧记录,发现该行记录已经有undo log了,那么最新的旧数据作为链表的表头,插在该行记录的undo log最前面

  • 修改该行age为30岁,并且修改隐藏字段的事务ID为当前事务2的ID, 那就是2,回滚指针指向刚刚拷贝到undo log的副本记录;

  • 事务提交,释放锁。

从上面,我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线性表,既链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录。


4. 读的时候是如何选择版本呢?

这就用到ReadView了。

Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的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所在的旧记录就是当前事务能看见的最新老版本。


如何比较:

读操作根据当前事务id与快照中事务系统状态做比较,判断数据对事务的可见性。


名称说明

rw_trx_ids: x当前ReadView创建时还在运行的事务id列表;

min_trx_id: 最小事务id, rw_trx_ids中最小的id;

max_trx_id: 最大事务ID,当前系统中已经生成的最大事务ID+1,即下一个要生成的事务ID;

creator_trx_id: 当前事务id。


数据可见性规则:

(1) 一行数据的DB_TRX_ID = creator_trx_id,表示数据是由当前事务修改的,可见。

(2) 一行数据的DB_TRX_ID < min_trx_id,表示数据在事务创建之前修改的,可见。

(3) 一行数据的DB_TRX_ID >= max_trx_id,表示数据在事务创建之后修改的,不可见。

(4) 一行数据的DB_TRX_ID大于等于min_trx_id且小于max_trx_id,如果trx_id在rw_trx_ids列表中,表示该事务还未提交(因为事务提交之后就被删除丢失了),不可见;否则可见。



现在我们来回到上面提到的问题:

1. MySQL innoDB是怎么实现可重复读的?
答:在RC隔离级别下,是每个快照读都会生成并获取最新的Read View,所以可能读到历史版本的数据。
而在RR隔离级别下,同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。


2. MySQL innoDB是怎么解决幻读的?  
MySQL innoDB通过Next-Key Lock解决幻读问题。行锁与间隙锁(锁定一个范围的记录, 但不包括记录本身)组合起来用就叫做Next-Key Lock。锁定一个范围,并且锁定记录本身。


最后再问个问题:

1. 我们知道select默认是不加锁的,那select…for update是如何实现锁的?

答:
(1) 简单的select操作是不加锁的,属于快照读,读取的可能是历史版本;
(2) select…for update是行锁、排它锁、悲观锁,属于当前读,它读取的是记录的最新版本。

参考文章:https://www.jianshu.com/p/8845ddca3b23

文章转载自程序媛的梦想,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论