MVCC
一、什么是MVCC
- 全称:Multi-Version Concurrency Control,多版本并发控制,是数据库通过控制数据行的历史版本,来控制并发场景下的一种手段。即使不需要加锁,也能控制并发情况下读写的冲突。做到即使在由读写冲突的情况下,也不用加锁,进行非阻塞并发读。MVCC中的读是
快照读
,而不是当前读
,快照读其实是乐观锁思想的一种,而当前读用的是悲观锁的思想。 - 在MySQL数据库中,默认的隔离级别是
可重复读
,该隔离级别下,是可以解决脏读和不可重复读
的问题,但是解决不了幻读
的问题。在隔离级别中,串行化
是可以解决幻读的,是从加锁的角度来解决的,大大降低了并发性能问题。因此MVCC
可以通过乐观锁的思想,在不加锁的非阻塞读来解决不可重复读和幻读的问题
二、快照读和当前读
1. 快照读
- 快照读是一次性读操作,就是不加锁的读。简单的SELECT语句不加锁,就是快照读。
- 由于快照读是通过版本来提高并发情况的非阻塞读,快照读取到的数据有可能不是最新的,可能是历史版本的数据。
- 在MVCC的快照读的读场景下,数据库的隔离级别不能是串行化,当隔离级别是串行化的时候,快照读会变成当前读。
- 快照读是基于MVCC的,避免了加锁操作的开销。
如下语句就是快照读:
SELECT * FROM student WHERE id = 1;
2. 当前读
- 当前读就是加锁的读,每次读取的到数据都是最新的。但在进行读操作的时候,会给记录行加锁,导致其他事务的
写
操作阻塞等待。 - 加锁的SELECT、写操作(INSERT、DELETE、UPDATE)都是当前读。
如下操作都是当前读:
SELECT * FROM student LOCK IN SHARE MODE;//查询语句加共享锁
SELECT * FROM student FOR UPDATE;//查询语句加排他锁
INSERT INTO student(id ,name) values(5,'李四'); //排他锁
DELETE FROM student WHERE id = 5; //排他锁
UPDATE student SET name = '李四' WHERE id = 1; //排他锁
三、MVCC的实现原理
原理:隐藏字段、undo log 、readView
1. 隐藏字段
数据库的每行数据,除了有我们自己定义的字段以外,还有三个隐藏的字段,分别是:DB_TRX_ID、DB_ROLL_POINTER、DB_ROWID
- trx_id:指每次事务对记录进行操作时,就将该事务的id赋值给trx_id隐藏列。
- roll_pointer:指的每次事务对记录进行操作时,会将旧版本存放在undo log日志中,roll_pointer就是存放一个地址指针,用来指向上一个版本,可通过这个指针,再配合undo log日志,找个上个版本的记录信息。
- rowid:指的是当我们没有给表指定主键时,就会把rowid作为当前表的主键。
2. undo log
undo log称为回滚日志,在insert、delete、update的时候生成。
- 在insert的时候,生成的版本日志,只有在事务回滚的时候用到,在事务提交后,就会立即把insert的uodo日志丢弃。
- update和delete的时候,每次记录修改成功后,就会将旧版本的记录存放到undo log日志中,再undo log日志中,每条旧版本记录都会有trxid和roll pointer,当记录的版本修改多次了,undo log日志就会生成该记录的多个版本,然后通过roll pointer将各个版本连接起来,形成一个链的形式,成为
版本链
。
3.Read View
3.1 什么时Read View
Read View是事务在使用MVCC进行快照读的时候,产生的读视图,在事务进行读记录的时候,会生成一个快照,为每个事务构建一个数组,用来存放当前活跃(指启动正在执行的事务,但还没有提交)的事务id,事务的id是递增的。
3.2 Read View结构设计
- Read View的主要作用就是用来判断,当前事务对那个版本的记录是可见的,当事务在进行这个记录的快照读时,就会得到这个记录的一个Read View,通过Read View去判断当前事务能读取那个记录版本的内容。这样就会出现可能读到这条记录的最新内容,也可能读到undo log日中的历史版本。
- Read View4个重要的参数:
- creator_trx_id:当前创建Read View的事务id。
- trx_list:记录在生成Read View的时候,当前系统中活跃的事务id。是一个数组,存放多个。
- up_limit_id:
trx_list
活跃id数组中,最小的事务id。 - low_limit_id:在生成Read View的时候,预分配下一个事务的id号。如:当前生成Read View的事务id为5,则下一个事务的id号应该为6,也就是low_limit_id=6。
3.2 Read View判断规则
判断规则主要先是拿up_limit_id 、creator_trx_id 、low_limit_id 以及trx_list中的trx_id
和当前记录中的trx_id进行比较。规则如下:
- 当记录版本的trx_id与
creator_trx_id
进行比较,如果相等,说明这条记录是自己更新的版本,因此可以访问这个版本的记录。 - 当记录版本的trx_id与
up_limit_id
比较,如果小于的话,则说明该版本是在生成Read View时候之前提交的,因此可以访问这个记录。 - 当前记录版本的trx_id与
low_limit_id
进行比较,如果大于等于的话,说明生成该版本的事务是在生成Read View之后才开启的,因此不能访问这个记录。 - 当前记录版本的trx_id在
up_limit_id和low_limit_id
区间内,需要进一步判断当前记录的版本id是否在trx_lis
t内:- 如果在,说明操作当前记录版本的事务还是活跃的,不能访问。
- 如果不在,说明说明操作当前记录版本的事务已经提交,能访问该版本的记录。
3.3 Read View整体执行流程
- 首先获取自己的的事务id。
- 获取Read View。
- 查询到页面记录数据,然后拿事务版本号与Read View的事务版本号进行对比。
- 如果Read View不符合,则需要从undo log日志中获取历史快照。
- 最后返回符合规则的结果。
下面看个示例:
- 首先开启事务A(事务id=5),后面再开启事务B(事务id=7)。
- 事务A对id为1的记录的name字段进行修改为【李四】,事务还未提交,事务B对id为1的记录的name字段进行修改为【王五】,也未提交。
- 此时事务C进行查询(事务id=0,说明:只要事务是查询操作,事务id都为0),此时是快照读,会生成Read View:
creator_trx_id =0,up_limit_id =5,low_limit_id =8,trx_list=[5,7]
。 - 进行Read View流程规则比较:
- 获取到最新的记录版本,name=王五,trx_id=7,该记录的tri_id 与
creator_trx_id
不匹配。 - 该记录的tri_id 比
up_limit_id
大,但比low_limit_id
小。 - 进一步判断是否,记录的tri_id是在
trx_list
活跃事务列表里面的,不符合规则,继续找下一个记录版本。 - 下个记录版本的name=李四,trx_id=5,通过Read View判断,该记录版本的tri_id是在
trx_list
活跃事务列表里面的,不符合规则,继续找下一个记录版本。 - 下个记录版本的name=张三,trx_id=2,进行Read View规则比较,
trx_id
是比up_limit_id
小的,因此可以访问该版本。 - 最后查询到的记录是name=张三的版本。
- 获取到最新的记录版本,name=王五,trx_id=7,该记录的tri_id 与
四、MVCC是如何解决幻读
首先MySQL的默认隔离级别是repeatable read,在MVCC时,同一个事务进行多次查询,都只会生成一次Read View
步骤:
- 事务A开启后,执行查询id>0的语句,会生成一个Read View:
creator_trx_id =5,up_limit_id =5,low_limit_id =8,trx_list=[5,7]
,此时记录的trx_id
为2,记录的trx_id小于up_limit_id ,进行Read View规则的计算后,判断该记录可以访问,查到【name=张三】的一条记录, - 在其间,开启事务B(id=7),事务B向表中插入了两条数据:李四、王五,并提交。
- 事务A再执行查询id>0的语句,此时不会再生成Read View,还是上次查询的Read View。
- 在查询的时候,发现多了两条记录,这两条记录的任务id=7。打算访问这两条记录时,进行Read View,发现这两条记录的事务id是处于
trx_list
活跃事务列表中,因此不能访问这两条新增的记录。 - 最后还是查到【name=张三】的一条记录。
五、总结
- 数据库在隔离级别为
READ COMMITTD
、REPEATABLE READ
的情况下,MVCC在快照读操作时,访问记录的版本链,使多并发事务对写-读和读-写
的性能得到提升。 - MVCC核心在于Read View,在
READ COMMITTD
、REPEATABLE READ
两种隔离级别下,在生成Read View的次数也不同。- READ COMMITTD:每执行一次SELECT语句的时候,都会重新生成一个Read View,这样会出现幻读的情况。
- REPEATABLE READ:在第一次执行SELECT语句的时候,就生成一个Read View,后面再执行SELECT语句的时候,不会再生成新的Read View,都会拿第一次生成的Read View去做判断。