一、什么是MVCC
MVCC,Multi-Version Concurrency Control,多版本并发控制。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。
MVCC只在已提交读(Read Committed)和可重复读(Repeatable Read)两个隔离级别下工作,MVCC的实现原理主要是依赖 每一行记录中两个隐藏字段,undo log,ReadView
一些概念
版本链
对于使⽤InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列:
-
trx_id:
每次⼀个事务对某条聚簇索引记录进⾏改动时,都会把该事务的事务id赋值给trx_id隐藏列。 -
roll_pointer:
每次对某条聚簇索引记录进⾏改动时,都会把旧的版本写⼊到undo⽇志中,然后这个隐藏列就相当于⼀个指针,可以通过它来找到该记录修改前的信息。
⽐⽅说我们的表hero现在只包含⼀条记录:
mysql>SELECT * FROM hero;
+--------+--------+---------+
|number | name | country |
+--------+--------+---------+
| 1 | 刘备 | 蜀 |
+--------+--------+---------+
假设插⼊该记录的事务id为80,那么此刻该条记录的示意图如下所示:
假设之后两个事务id分别为100、200的事务对这条记录进⾏UPDATE操作,操作流程如下:
每次对记录进⾏改动,都会记录⼀条undo⽇志,每条undo⽇志也都有⼀个roll_pointer属性(INSERT操作对应的undo⽇志没有该属性,因为该记录并没有更早的版本),可以将这些undo⽇志都连起来,串成⼀个链表,所以现在的情况就像下图⼀样:
这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含⽣成该版本时对应的事务id
ReadView
对于使⽤READ UNCOMMITTED隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了;
对于使⽤SERIALIZABLE隔离级别的事务来说,规定使⽤加锁的⽅式来访问记录;
对于使⽤READ COMMITTED和REPEATABLE READ隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另⼀个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核⼼问题就是:需要判断⼀下版本链中的哪个版本是当前事务可⻅的。为此提出了⼀个ReadView的概念。
ReadView中主要包含4个⽐较重要的内容:
- m_ids:表示在⽣成ReadView时当前系统中活跃的读写事务的事务id列表(未提交的事务)。
- min_trx_id:表示在⽣成ReadView时当前系统中活跃的读写事务中最⼩的事务id,也就是m_ids中的最⼩值。
- max_trx_id:表示⽣成ReadView时系统中应该分配给下⼀个事务的id值。
注意max_trx_id并不是m_ids中的最⼤值,事务id是递增分配的。⽐⽅说现在有id为1,2,3这三个事务,之后id为3的事务提交了。那么⼀个新的读事务在⽣成ReadView时,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4。
- creator_trx_id:表示⽣成该ReadView的事务的事务id。
只有在对表中的记录做改动时(执⾏INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在⼀个只读事务中的事务id值都默认为0。
有了这个ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可⻅:
-
如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它⾃⼰修改过的记录,所以该版本可以被当前事务访问。
-
如果被访问版本的trx_id属性值⼩于ReadView中的min_trx_id值,表明⽣成该版本的事务在当前事务⽣成ReadView前已经提交,所以该版本可以被当前事务访问。
-
如果被访问版本的trx_id属性值⼤于ReadView中的max_trx_id值,表明⽣成该版本的事务在当前事务⽣成ReadView后才开启,所以该版本不可以被当前事务访问。
-
如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间,那就需要判断⼀下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时⽣成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时⽣成该版本的事务已经被提交,该版本可以被访问。
如果某个版本的数据对当前事务不可⻅的话,那就顺着版本链找到下⼀个版本的数据。
READ COMMITTED和REPEATABLE READ隔离级别的的⼀个⾮常⼤的区别就是它们⽣成ReadView的时机不同:
- READ COMMITTED —— 每次读取数据前都⽣成⼀个ReadView
- REPEATABLE READ —— 在第⼀次读取数据时⽣成⼀个ReadView
示例
READ COMMITTED级别
⽐⽅说现在系统⾥有两个事务id分别为100、200的事务在执⾏:
# Transaction 100
BEGIN;
UPDATE hero SET name = '关⽻' WHERE number = 1;
UPDATE hero SET name = '张⻜' WHERE number = 1;
# Transaction 200
BEGIN;
# 更新了⼀些别的表的记录
...
再次强调⼀遍,事务执⾏过程中,只有在第⼀次真正修改记录时(⽐如使⽤INSERT、DELETE、UPDATE语句),才会被分配⼀
个单独的事务id,这个事务id是递增的。所以我们才在Transaction 200中更新⼀些别的表的记录,⽬的是让它分配事务id。
此刻,表hero中number为1的记录得到的版本链表如下所示:
假设现在有⼀个使⽤READ COMMITTED隔离级别的事务开始执⾏:
# 使⽤READ COMMITTED隔离级别的事务
BEGIN;
# SELECT1:Transaction 100、200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'
这个SELECT1的执⾏过程如下:
-
在执⾏SELECT语句时会先⽣成⼀个ReadView,ReadView的m_ids列表的内容就是[100, 200],min_trx_id为100,max_trx_id为201,creator_trx_id为0。
-
然后从版本链中挑选可⻅的记录,从图中可以看出,最新版本的列name的内容是'张⻜',该版本的trx_id值为100,在m_ids列表内,所以不符合可⻅性要求,根据roll_pointer跳到下⼀个版本。
-
下⼀个版本的列name的内容是'关⽻',该版本的trx_id值也为100,也在m_ids列表内,所以也不符合要求,继续跳到下⼀个版本。
-
下⼀个版本的列name的内容是'刘备',该版本的trx_id值为80,⼩于ReadView中的min_trx_id值100,所以这个版本是符合要求的,最后返回给⽤户的版本就是这条列name为'刘备'的记录。
之后,我们把事务id为100的事务提交⼀下,然后再到事务id为200的事务中更新⼀下表hero中number为1的记录:
# Transaction 200
BEGIN;
# 更新了⼀些别的表的记录
...
UPDATE hero SET name = '赵云' WHERE number = 1;
UPDATE hero SET name = '诸葛亮' WHERE number = 1;
此刻,表hero中number为1的记录的版本链就⻓这样:
然后再到刚才使⽤READ COMMITTED隔离级别的事务中继续查找这个number为1的记录,如下:
# 使⽤READ COMMITTED隔离级别的事务
BEGIN;
# SELECT1:Transaction 100、200均未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'
# SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'张⻜'
这个SELECT2的执⾏过程如下:
-
在执⾏SELECT语句时会⼜会单独⽣成⼀个ReadView,该ReadView的m_ids列表的内容就是[200](事务id为100的那个事务已经提交了,所以再次⽣成快照时就没有它了),min_trx_id为200,max_trx_id为201,creator_trx_id为0。
-
然后从版本链中挑选可⻅的记录,从图中可以看出,最新版本的列name的内容是'诸葛亮',该版本的trx_id值为200,在m_ids列表内,所以不符合可⻅性要求,根据roll_pointer跳到下⼀个版本。
-
下⼀个版本的列name的内容是'赵云',该版本的trx_id值为200,也在m_ids列表内,所以也不符合要求,继续跳到下⼀个版本。
-
下⼀个版本的列name的内容是'张⻜',该版本的trx_id值为100,⼩于ReadView中的min_trx_id值200,所以这个版本是符合要求的,最后返回给⽤户的版本就是这条列name为'张⻜'的记录。