一、概述
MVCC (MultiVersion Concurrency Control) 叫做多版本并发控制机制。主要是通过数据多版本来实现读-写分离,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读,从而提高数据库并发性能。
MVCC只在已提交读(Read Committed)和可重复读(Repeatable Read)两个隔离级别下工作,其他两个隔离级别和MVCC是不兼容的。因为未提交读,总数读取最新的数据行,而不是读取符合当前事务版本的数据行。而串行化(Serializable)则会对读的所有数据多加锁。
主要思想是:InnoDB通过undo log保存每条数据的多个版本,并且能够找回数据历史版本提供给用户读,每个事务读到的数据版本可能是不一样的。在同一个事务中,用户只能看到该事务创建快照之前已经提交的修改和该事务本身做的修改。
二、当前读和快照读
- 快照读:读取的是历史数据,不加锁的简单 Select 都属于快照读。
bash
select * from user where id = 1;
- 当前读:读取的是最新数据,而不是历史的数据,加锁的 SELECT,或者对数据进行增删改都会进行当前读。
bash
SELECT * FROM user LOCK IN SHARE MODE;
SELECT * FROM user FOR UPDATE;
INSERT INTO user values ...
DELETE FROM user WHERE ...
UPDATE user SET ...
三、MVCC 与快照读的关系
MVCC 多版本并发控制是维持一个数据的多个版本,使得读写操作没有冲突,而这只是一个抽象概念。而快照读就是 MySQL 实现 MVCC 理想模型的其中一个非阻塞读功能,快照读所读取的就是MVCC维持的多版本数据中去某个版本数据。
四、MVCC 优点
- 提供多版本数据,在并发读写数据库时,读写操作互不影响,提高了数据库并发读写的性能。
- 可以解决脏读,幻读,不可重复读等事务隔离问题。
五、MVCC 实现原理
主要是依赖每一行记录中2个隐藏字段、undo log版本链 以及 ReadView。
1. 行记录的隐藏字段
- trx_id:操作这个数据的事务 ID,也就是最后一个对数据插入或者更新的事务 ID 。
- roll_ptr:回滚指针,指向这个记录的 Undo Log 信息。
- row_id:隐藏的行 ID ,用来生成默认的聚集索引。如果创建数据表时没指定聚集索引,这时 InnoDB 就会用这个隐藏 ID 来创建聚集索引。
2. undo log版本链
多个事务并行操作某一行数据时,不同事务对该行数据的修改会产生多个版本,然后通过回滚指针(roll_pointer),连成一个链表,这个链表就称为版本链。(其实undo log 版本链的节点在事务提交之后可能是会 purge 线程清除掉的)
(1)首先开启第一个事务,事务id为1,通过insert语句往表中插入一条新记录如下:
由于是新插入的记录,因此它的roll_pointer指向的undo log为空。
(2)接着开启第二个事务,事务id为2,更新该行记录的name为Tom。
- 在第二个事务修改该行记录时,数据库会先对该行加排他锁;
- 然后把该行数据拷贝到 undo log 中,作为旧版本记录;
- 拷贝完毕后,再修改该行name为Tom,并且修改隐藏字段trx_id为当前事务的id, 回滚指针roll_pointer指向拷贝到 undo log 的副本记录,既表示我的上一个版本就是它。
- 事务提交后,释放锁。
(3)再开启第三个事务,事务id为3,修改改行记录的age为25。
- 在第三个事务修改该行记录时,数据库会先对该行加排他锁;
- 然后把该行数据拷贝到 undo log 中,作为旧版本记录;
- 拷贝完毕后,再修改该行age为25,并且修改隐藏字段trx_id为当前事务的id, 回滚指针roll_pointer指向拷贝到 undo log 的副本记录,既表示我的上一个版本就是它。
- 事务提交后,释放锁。
3.ReadView
Read View 就是事务进行 "快照读" 操作时候生产的 "读视图",在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID。主要是用来做可见性判断的,即当我们某个事务执行快照读的时候,当前事务能够看到undo log版本链上的哪个版本数据。
(1)ReadView中主要属性
- trx_ids:当前系统中那些活跃(未提交)的读写事务ID,它数据结构为一个List。(trx_ids中的活跃事务不包括:当前事务自己)。
- min_trx_id:trx_ids中最小的事务id。
- max_trx_id:预分配事务编号,MySQL即将为下一个事务分配的事务id。
- creator_trx_id: 表示生成该 ReadView 的事务的 事务id。
(2)ReadView可见性判断规则
- 判断 trx_id = creator_trx_id,如果条件成立,则说明当前事务与进行select快照读的事务是同一个事务,那么该select是可以读取到该trx_id对应的版本;如果条件不成立,继续往下判断其他条件。
- 判断 trx_id < min_trx_id,如果条件成立,则说明当前事务是在进行select快照读的事务之前就已经提交了,那么该select是可以读取到该trx_id对应的版本;如果条件不成立,继续往下判断其他条件。
- 判断 trx_id > max_trx_id,如果条件成立,则说明当前事务是在进行select快照读的事务之后才创建的,那么该select是肯定读取不到该trx_id对应的版本;如果条件不成立,则继续往下判断其他条件。
- 判断 min_trx_id < trx_id < max_trx_id,如果条件成立,则说明当前事务可能在活跃的事务中,需要再判断下trx_id是否在trx_ids中,如果不在,则说明当前事务已经提交过了,那么该select就可以读取到该trx_id对应的版本;否则说明当前事务还未提交,该select也就无法读取到该trx_id对应的版本。
(3)已提交读(RC)与可重复读(RR)级别下的快照读
RC 隔离级与RR隔离级生成Read View 的时机是不同的,因为也造成了两者快照读的结果的不同。在 RC 隔离级别下,是每个快照读都会生成并获取最新的 Read View;而在 RR 隔离级别下,则是同一个事务中的第一个快照读才会创建 Read View, 之后的快照读获取的都是同一个 Read View。
undo log版本链:
ReadView创建实时机:
解析:
(1)RC级别下:
在事务D中的第一个select语句执行时,生成的ReadView中,当前系统中那些活跃(未提交)的读写事务有{3,4},创建ReadView的事务id为5,活跃事务中最新事务id为3,预分配的事务id为6。再根据undo log版本链依次与ReadView的可见性判断规则进行对比,判断哪个版本是可见的。例如:
- 先对版本链最新的一个版本trx_id = 4进行判断:
1.先判断 trx_id = creator_trx_id:creator_trx_id = 5,trx_id = 4,条件不成立,暂时无法证明该版本相对于当前select可见,则继续根据其他规则进行判断。
2.继续判断 trx_id < min_trx_id:min_trx_id = 3,trx_id = 4,条件不成立,也暂时无法证明该版本相对于当前select可见,则继续根据其他规则进行判断。
3.继续判断 trx_id > max_trx_id:max_trx_id = 6,trx_id = 4,条件不成立,也暂时无法证明该版本相对于当前select可见,则需要继续据其他规则进行判断。(如果该条件成立,则可以证明该版本相对于当前select肯定是不可见的)
4.继续判断 min_trx_id < trx_id < max_trx_id:条件成立,说明当前事务可能在活跃的事务中,需要再判断下trx_id是否在trx_ids中,trx_id = 4在活跃的事务中,也就是当前事务还未提交,那么该select也就无法读取到该版本的数据。
所有可见性规则判断完毕,最终得出版本链中最新的一个事务C这个版本在事务D中的第一个select语句是不可见的。 - 再对版本链中trx_id = 3 按照上述的可见性规则依次判断:最终得出该版本在事务D中的第一个select语句也是不可见的。
- 再对版本链中trx_id = 2 按照上述的可见性规则依次判断:最终得出该版本在事务D中的第一个select语句也是可见的。
在事务D中的第二个select语句执行时,会重新生成一个新的ReadView,此时系统中那些活跃(未提交)的读写事务有{4},创建ReadView的事务id为5,活跃事务中最新事务id为4,预分配的事务id为6。再根据undo log版本链依次与ReadView的可见性判断规则进行对比,可以得出最终该select语句的可见版本为 trx_id = 3 和 trx_id = 2,与第一个select语句查询的可见版本不一样。这也是为什RC隔离级别下,同一事务中多次使用快照读查询时会产生查询结果不一致的问题。
(2)RR级别下:
在事务D中的第一个select语句执行时,生成一个ReadView与RC级别是一样的,所以最终得到的可见版本也是只有 trx_id = 2。但是在事务D中的第二个select语句执行时,RR级别不会再生成新的ReadView,而是直接复用第一次生的的那个ReadView,所以最终判断下来,第二个select语句的可见版本也只有 trx_id = 2。这也就是为什么RR级别下同一事务中多次使用快照读查询时结果都是相同的原因。如何在事务中使用了当前读,则后面的快照读不会再复用之前的ReadView,而是重新生成一个新的ReadView,这也RR级别下无法解决幻读的原因。