1、什么是MVCC?
MVCC,Multi-Version Concurrency Control,多版本并发控制,是MySQL的默认存储引擎InnoDB实现隔离级别的一种具体方式,能够实现提交读/READ-COMMITTED和可重复读/REPEATABLE-READ两种隔离级别。
MVCC指的就是在使用 READ COMMITTDREPEATABLE READ这两种隔离级别的事务在执行普通的 SELECT 操作时,访问记录的版本链的过程。可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。
- 提交读/READ-COMMITTED:允许读取并发事务已经提交的数据, 可以阻止脏读,但是幻读或不可重复读仍有可能发生。
- 可重复读/REPEATABLE-READ:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
2、版本链和ReadView
(1)版本链
对于使用 InnoDB 存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列:
- trx_id:一个是事务ID,保存修改此条记录的事务ID
- roll_pointer:一个是回滚指针,指向该记录修改前的信息
(2)ReadView
- 对使用READ UNCOMMITTED隔离级别的事务,直接读取记录的最新版
- 对使用SERIALIAZABLE隔离级别的事务,使用加锁来读取记录
- 对使用READ-COMMITTED和REPEATABLE-READ隔离级别的事务,需要使用版本链读记录
核心问题:判断版本链中的哪个版本是当前事务可见的?
-----------------------------------
ReadView的四个主要内容:
- m_ids:表示在生成 ReadView 时,当前系统中活跃的读写事务的事务 id 列表。
- min_trx_id:表示在生成 ReadView 时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小 值。
- ****:表示生成 ReadView 时系统中应该分配给下一个事务的id值。
- creator_trx_id:表示生成该 ReadView 的事务的事务id。
----------------------------------
READ COMMITED 在每一次进行普通 SELECT 操作前都会生成一个ReadView,而 REPEATABLE READ 只在 第一次进行普通 SELECT 操作前生成一个 ReadView ,之后的查询操作都重复使用这个 ReadView。
----------------------------------
通过比较版本链中被访问版本的trx_id和ReadView中的creator_trx_id /min_trx_id/max_trx_id来判断当前记录是否可以被此事务访问
1、trx_id == creator_trx_id相同,则当前事务在访问它自己修改过的记录,该版本可以被当前事务访问;
2、trx_id < min_trx_id,表明生成该版本的事务在当前事务生成ReadView之前已经提交了,该版本可以被当前事务访问;
3、trx_id > max_trx_id,证明生成该版本的事务,在当前事务生成ReadView后才开启,该版本不能被当前事务访问;
4、 min_trx_id < trx_id < max_trx_id,需要继续判断 trx_id是否在m_ids列表中
1)在,说明创建ReadView时生成该版本的事务还是活跃的,不能被访问
2)不在,说明创建ReadView时生成该版本的事务已提交,该版本能被访问
3、MVCC带来的好处
-数据多版本(MVCC)是MySQL实现高性能的一个主要的方式,通过对普通的SELECT不加锁,直接利用MVCC读取指定版本的值,避免了对数据重复加锁的过程.
- 在 READ COMMITED 事务隔离级别下,对于快照数据,读取被锁定行的最新一份快照数据
- 在 REPEATABLE READ事务隔离级别下,读取事务开始时的行数据版本
在实际场景中读操作往往多于写操作,而 MVCC 利用了多版本的思想,写操作更新最新的版本快照,而读操作去读旧版本快照,没有互斥关系。
- 在 MVCC 中事务的修改操作(DELETE、INSERT、UPDATE)会为数据行新增一个版本快照。
- 可以认为MVCC是行级锁的一个变种,InnoDB采用了乐观锁的策略,在每行记录保存两个隐藏列来实现,这两个列保存了行的版本号信息,每开启一个新事务,版本号自动更新,事务开始时刻的版本号作为事务的版本号。用来和查询到的记录所带的版本号进行比较来判断。
在事务进行读取操作时,为了解决脏读和不可重复读问题,MVCC 规定只能读取已经提交的快照。当然一个事务可以读取自身未提交的快照,这不算是脏读。
– 系统版本号 SYS_ID:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
– 事务版本号 TRX_ID :事务开始时的系统版本号。
4、Undo日志
MVCC 的多版本指的是多个版本的快照,快照存储在 Undo 日志中,该日志通过2.1介绍的回滚指针 ROLL_PTR 把一个数据行的所有快照连接起来。
- 例如在 MySQL 创建一个表 t,包含主键 id 和一个字段 x。我们先插入一个数据行,然后对该数据行执行两次更新操作。
INSERT INTO t(id, x) VALUES(1, "a");
UPDATE t SET x="b" WHERE id=1;
UPDATE t SET x="c" WHERE id=1;
因为没有使用 START TRANSACTION
将上面的操作当成一个事务来执行,根据 MySQL 的 AUTOCOMMIT 机制,每个操作都会被当成一个事务来执行,所以上面的操作总共涉及到三个事务。快照中除了记录事务版本号 TRX_ID 和操作之外,还记录了一个 bit 的 DEL 字段,用于标记是否被删除。

INSERT、UPDATE、DELETE 操作会创建一个日志,并将事务版本号 TRX_ID 写入。DELETE 可以看成是一个特殊的 UPDATE,还会额外将 DEL 字段设置为 1。
5、ReadView的结构
MVCC 维护了一个 ReadView 结构,主要包含了当前系统未提交的事务列表 TRX_IDs {TRX_ID_1, TRX_ID_2, …},还有该列表的最小值 TRX_ID_MIN 和 TRX_ID_MAX。

在进行 SELECT 操作时,根据数据行快照的 TRX_ID 与 TRX_ID_MIN 和 TRX_ID_MAX 之间的关系,从而判断数据行快照是否可以使用:
-
TRX_ID < TRX_ID_MIN,表示该数据行快照时在当前所有未提交事务之前进行更改的,因此可以使用。
-
TRX_ID > TRX_ID_MAX,表示该数据行快照是在事务启动之后被更改的,因此不可使用。
-
TRX_ID_MIN <= TRX_ID <= TRX_ID_MAX,需要根据隔离级别再进行判断:
- 提交读:如果 TRX_ID 在 TRX_IDs 列表中,表示该数据行快照对应的事务还未提交,则该快照不可使用。否则表示已经提交,可以使用。
- 可重复读:都不可以使用。因为如果可以使用的话,那么其它事务也可以读到这个数据行快照并进行修改,那么当前事务再去读这个数据行得到的值就会发生改变,也就是出现了不可重复读问题。
在数据行快照不可使用的情况下,需要沿着 Undo Log 的回滚指针 ROLL_PTR 找到下一个快照,再进行上面的判断。
6、快照读和当前读### 1. 快照读
(1)快照读
MVCC 的 SELECT 操作是快照中的数据,不需要进行加锁操作。
SELECT * FROM table ...;
(2)当前读
MVCC 其它会对数据库进行修改的操作(INSERT、UPDATE、DELETE)需要进行加锁操作,从而读取最新的数据。可以看到 MVCC 并不是完全不用加锁,而只是避免了 SELECT 的加锁操作。
INSERT;
UPDATE;
DELETE;
在进行 SELECT 操作时,可以强制指定进行加锁操作。以下第一个语句需要加 S 锁,第二个需要加 X 锁。
SELECT * FROM table WHERE ? lock in share mode;
SELECT * FROM table WHERE ? for update;