什么是MVCC?
MCVV(Multiversion Concurrency Control),多版本并发控制是InnoDB引擎处理读写冲突的手段,目的是用来提高数据库并发场景下的吞吐性能。
不同的事务在并发过程中,SELECT操作可以不加锁,而是通过MVCC机制来指定读取版本,通过一些手段来保证读取的数据符合事务隔离级别,从而解决并发场景下的读写冲突
版本链
又称事务链,每次修改数据的时候,都会记录一条undoLog日志,日志中记录数据每一次的变化,并且通过数据中的两个隐藏列,trx_id(事务id)用标记每一条数据对应的事务id,roll_pointer(指针)用来串联版本链的指针。
ReadView
阅读视图,可以理解为某一次读取的时候,根据不同的事务生成的数据快照,并且除了表中的数据,还有一下这些固定字段。
m_ids:表示生成ReadView时,当前系统正在活跃的读写事务的事务Id列表。
min_trx_id:表示生成ReadView时,当前系统中活跃的读写事务的最小事务Id。
max_trx_id:表示生成ReadView时,当前时间戳InnoDB将在下一次分配的事务id。
creator_trx_id:当前事务id。
原理
我们都知道,数据库的事务有四个隔离级别:读未提交(Read Uncommitted,简称RU)、读已提交(Read Committed,简称RC)、可重复读(Repeatable Read,简称RR)、串行化(Serializable)。
只有RC和RR才跟MVCC机制有关,因为RU会直接返回记录上的最新值,而Serializable是每一次操作都要加锁。
RC和RR的事务隔离就是通过版本链来控制的,核心逻辑就是判断版本链中的那个版本是当前事务可见可处理的,
举个例子,当前有个数据的初始值是 name=“刘德华”
下面是事务时间表,有这样5个事务
时间 | tx_100 | tx_200 | tx_300 | tx_400 | tx_500 |
---|---|---|---|---|---|
T1 | begin; | ||||
T2 | begin; | begin; | |||
T3 | update user set name = “古天乐” where id = 1 | ||||
T4 | select * from user where id = 1 | begin; | |||
T5 | commit; | update user set name = “刘青云” where id = 1 | |||
T6 | select * from user where id = 1 | select * from user where id = 1 | begin; | ||
T7 | commit; | ||||
T8 | select * from user where id = 1 | select * from user where id = 1 |
当在T4时间点的时候,版本链如下:
id | name | trx_id | roll_pointer |
---|---|---|---|
1 | 刘德华 | 0 | null |
1 | 古天乐 | 100 | roll_pointer(指向上一个版本) |
RC隔离级别
当在读已提交(Read Committed)隔离级别的时候,每一次读取都会生成一个新的ReadView。
当处于T4时间点,由于tx_100事务未提交,tx_200事务未操作,事务 tx_300 查询出的数据就是 name=“刘德华”。
id | name | trx_id | roll_pointer |
---|---|---|---|
1 | 刘德华 | 0 | null |
1 | 古天乐 | 100 | roll_pointer(指向上一个版本) |
当处于T6时间点,tx_100事务已提交,tx_200事务未提交,那么tx_300事务查询出来的数据就是name=“古天乐”。tx_400事务也是一样。
id | name | trx_id | roll_pointer |
---|---|---|---|
1 | 刘德华 | 0 | null |
1 | 古天乐 | 100 | roll_pointer(指向上一个版本) |
1 | 刘青云 | 200 | roll_pointer(指向上一个版本) |
当处于T8时间点,tx_100、tx_200事务都已提交,所以tx_300、tx_500事务查询到的数据都是name=“刘青云”。
id | name | trx_id | roll_pointer |
---|---|---|---|
1 | 刘德华 | 0 | null |
1 | 古天乐 | 100 | roll_pointer(指向上一个版本) |
1 | 刘青云 | 200 | roll_pointer(指向上一个版本) |
有没有发现一个问题,在tx_300事务中,虽然是同一个事务下,但是每次查询的数据都不一样,这就是出现了不可重复读的现象。
RR隔离级别
当处于可重复读(Repeatable Read)隔离级别的时候,与RC最大的不同就是同一个读取事务,只有一个ReadView。
事务tx_300查询了3次,第一次查询的时候是在T4时间点,tx_100事务还未提交数据,所以生成了一个快照ReadView1,name=“刘德华”。
T6时间点事务tx_300第二次查询的时候不会生成新的快照,依旧获取的是ReadView1,name=“刘德华”。 而此时查询的事务tx_400,则会生成快照ReadView2,name=“古天乐”。
T8时间点的时候,事务tx_300第三次查询依旧不会生成新的快照,获取的数据还是ReadView1,name=“刘德华”。 此时查询事务tx_500,则会生成新的快照ReadView3,name=“刘青云”。
MVCC机制,就是通过版本链来控制每一个事务读取的版本,从而实现事务的隔离级别。关键是要理解,如何通过控制ReadView的生成,来实现不同的事务隔离级别。
当前读
读取的是最新版本,并且对数据加锁,阻塞其他操作修改记录。
比如select… update,update,delete,insert这类操作都是当前读
快照读
只有select单纯的读取操作,会在读取的时候生成ReadView快照数据。
并且RC隔离级别下,会在每次select操作的时候都生成一个ReadView快照。
而在RR隔离级别下,同一个事务中只会在第一次select操作的时候生成ReadView快照,避免同一个事务多次读取的数据不一样。