一、什么是MVCC?
MVCC(Multi-Version Concurrency Control)多版本并发控制,是确保在高并发下,多个事务读取数据时不加锁也可以多次读取相同的值。MVCC在读已提交(READ COMMITTED)、可重复读(REPEATABLE READ 简称RR)模式下才生效。
二、MVCC解决了事务的什么问题?
在并发事务下,可能会产生如下问题:
1.脏读 :当前事务读取到其它事务未提交的数据。
2.脏写 : 事务B提交后,将事务A提交的数据覆盖。
3.不可重复读:在同一个事务中,不同时间段执行相同的查询语句,得到的结果集不相同。
4.幻读:事务A读取到了事务B新增的数据。
MVCC可重复读模式下,解决了事务的脏读、脏写、不可重复读等问题,但是还是存在幻读问题,幻读问题可以使用间隙锁进行解决。
三、MVCC的实现原理
3.1 undo log文件
MVCC是通过历史操作快照的版本链进行实现的,这个版本链实际上就是undo log的记录的数据。
undo log里面通过两个隐藏字段trx_id、roll_pointer将这些数据串联起来,形成一个历史版本链。当事务需要回滚或事务需要执行select语句时,都是通过undo log里面的roll_pointer去查找对应版本的数据。
3.2 read view一致性视图
当事务要执行查询sql时,会生成一个一致性视图,也就是read view,read view由当前所有未提交的事务ID组成的一个数组,和已创建的最大事务id构成。
在这个数组中,最小的事务id称为min_id,最大的事务id称为max_id,当要查询历史数据时,需要通过对比事务的id才能区分哪些版本的数据可以展示,哪些数据不能展示。
示例:read view = [100,101],300;
其中,Min_id = 100、Max_id = 300。100和101都属于未提交的活跃事务。
在可重复读模式下,只会在第一次执行查询语句时生成read view,而在读已提交模式下,每次查询都会生成一个read view。
3.3 redo log 版本链对比规则
Mysql将整个版本链的数据划分为三个区间:
如果trx_id < min_id,那么表示这个事务是属于已提交的事务,数据是可见的。
如果trx_id > max_id,那么标识这个事务是属于将来要开启的,数据是不可见的。
如果min_id <= trx_id <= max_id ,就要分为两种情况:
a.如果trx_id在read view数组中,那么就表示当前trx_id属于未提交的事务,数据是不可见的。
b.如果trx_id不在read view数组中,那么当前trx_id就是属于已提交的事务,数据是可见的。
正是有了MVCC机制,让多个事务对同一条数据进行读写时,不需要加锁也不会出现读写冲突。事务A每次读的都是历史版本的快照,而事务B修改的数据,则会称为这个版本链的最新数据,通过上面的对比规则,其它事务是不会读取到最新的已修改的值。
但是MVCC没有解决幻读问题,对于其它事务新增的数据,虽然读不到,但是仍然可以感知到新增的数据,比如对新增数据进行修改操作。在可重复读模式下,可以使用间隙锁进行解决幻读问题。
3.4 间隙锁 Gap Lock
间隙锁可以理解成范围锁,锁的是两个值之间的范围,其它事务无法对范围内的数据进行新增和修改。
比如事务A当前的间隙锁的值是7-10,但是目前数据库中并不存在id =8的数据,事务B想要新增id =8的数据,此时是无法新增的,这样就可以解决幻读问题。
四、数据库中的锁
4.1 数据库的锁是什么?
在并发场景下,锁是协调并发访问资源的一种手段,通过加锁阻塞方式,让一个资源在同一时刻只能有一个客户端去访问。
4.2 锁的分类
a.从性能上分
悲观锁:对要访问的资源直接加锁
乐观锁:通过版本对比的方式来实现,不会对访问的资源加锁
b.从操作类型分
读锁(shared 共享锁):针对同一行数据,可以允许多个读,但是会阻塞写锁。
写锁(exclusive 排它锁):对于同一行数据,会阻塞其它事务的读和写。
c.从锁的粒度上分
表锁:开销小、加锁快,不会出现死锁问题,锁的粒度大,并发度低。
行锁:开销大、加锁慢,会出现死锁问题,锁的粒度小,并发度高。
一句话总结:读锁不会阻塞读,会阻塞写;写锁会阻塞读写。
行锁:InnoDB中的行锁是加在索引上的,不是针对数据行进行加锁。如果该索引失效,行锁会升级成表锁。
五、总结
MVCC:
1、MVCC在不加锁的情况下,解决了并发事务的脏读、脏写不可重复度等问题,而幻读可以使用间隙锁进行解决。
2、undo log里面通过两个隐藏字段trx_id、roll_pointer将历史快照数据串联起来,形成一个版本链,是read view获取数据的前提。
3、read view是在第一次查询时生成的,由所有未提交的活跃事务id组成的数组和最大事务id构成。
4、通过对比事务id的大小,将数据进行展示。
锁:
InnoDB中,行锁是加在索引上,因此要注意索引的失效问题,否则行锁升级成表锁。
尽量减少条件检索范围,避免间隙锁的范围过大,降低并发力度。