InnoDB存储引擎对MVCC的实现
一、概况
MVCC (Multiversion Concurrency Control),多版本并发控制。
顾名思义,MVCC 是通过数据行的多个版本管理来实现数据库的 并发控制 。这项技术使得在InnoDB的事务隔离级别下执行 一致性读 操作有了保证。
二、前置概念
MVCC
的实现依赖于:隐藏字段、Read View、undo log
1、隐藏字段
InnoDB
存储引擎为每行数据
添加了三个隐藏字段
DB_ROW_ID:如果没有设置主键
且该表没有唯一非空索引
时,InnoDB
会使用DB_ROW_ID
来生成聚簇索引
DB_TRX_ID:表示最后一次插入或更新该行的事务 id。
DB_ROLL_PTR:回滚指针,每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到 undo日志 中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
2、Read View
Read View主要是用来做可见性判断,使用 READ COMMITTED
和 REPEATABLE READ
隔离级别的事务,都必须保证读到 已经提交了的
事务修改。
ReadView中主要以下比较重要的字段
creator_trx_id:创建这个 Read View 的事务 ID。
说明:只有在对表中的记录做改动时(执行INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在一个只读事务中的事务id值都默认为0。
trx_ids:表示在生成
ReadView
时当前系统中活跃的读写事务的 事务id列表,即DB_TRX_ID列表 。up_limit_id:活跃的事务中最小的事务 ID
low_limit_id:表示生成ReadView时整个系统中应该分配给下一个事务的 id 值
3、undo log
每一行记录每次更新后,都会将旧值
放到一条 undo日志 中,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,我们把这个链表称之为 版本链
知识点:
在 InnoDB 存储引擎中
undo log
分为两种:
insert undo log:指在
insert
操作中产生的undo log
。因为 insert 操作的记录只对事务本身可见,对其他事务不可见,故该undo log
在事务提交后直接删除
update undo log:
update
或delete
操作中产生的undo log
。提交时放入undo log
链表,等待purge线程
进行最后的删除
三、执行流程
MVCC主要是针对READ COMMITTED
和 REPEATABLE READ
隔离级别的事务,因为READ UNCOMMITTED
直接读取最新的数据即可,SERIALIZABL
使用加锁的方式保持访问控制
1、在READ COMMITTED隔离级别下示例说明
注意:每次读取数据前都生成一个ReadView
①现在student 表中有一条事务ID为8
的插入的数据,并且事务已提交
②有两个 事务id 分别为 10 、 20 的事务在执行
# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;
# Transaction 20
BEGIN;
# 更新了一些别的表的记录,为了确保DB_TRX_ID有值
③另外一个事务A,查询id为1的记录
BEGIN;
SELECT * FROM student WHERE id = 1; #得到的列name的值为'张三'
此刻,表student中 id 为 1 的记录的版本链就长这样,因为只有更新操作才会存在于undo log
中
解析过程
当事务A执行查询的时候,会生成如下的
ReadView
creator_trx_id(当前分配的事务ID)值为0
trx_ids(当前活跃的事务ID列表)值为[10,20]
up_limit_id(最小活跃事务ID)值为10
low_limit_id(下一个要分配的事务ID)值为21(假设该过程中没有其他事务开启)
在事务A中
根据
undo log
日志链发现,第一条事务id为10,在当前活跃事务trx_ids中,说明事务还未提交,继续向下查找,直到找到小于up_limit_id的记录,即事务ID为8的记录
,则name为张三的记录被查询出来了
④将Transaction 10
提交 Transaction 20
修改一下数据
# Transaction 10
COMMIT;
# Transaction 20
UPDATE student SET name="钱七" WHERE id=1;
UPDATE student SET name="宋八" WHERE id=1;
⑤事务A继续查询id为1的记录
SELECT * FROM student WHERE id = 1;
此刻,表student中 id 为 1 的记录的版本链长这样
解析过程
当事务A再次执行查询的时候,会重新生成如下的ReadView
creator_trx_id(当前分配的事务ID)值为0
trx_ids(当前活跃的事务ID列表)值为[20]
up_limit_id(最小活跃事务ID)值为20
low_limit_id(下一个要分配的事务ID)值为21(假设该过程中没有其他事务开启)
在事务A中
根据
undo log
日志链发现,第一条事务id为20,在当前活跃事务trx_ids中,说明事务20还未提交,继续向下查找,直到找到小于up_limit_id的记录,即事务ID为10并且name为王五的记录
,则name为王五的记录被查询出来了
2、在REPEATABLE READ隔离级别下示例说明
注意:只有第一次读取数据时会产生一个ReadView
还是READ COMMITTED上面的过程,第二次查询结果还是使用第一次的ReadView,所以读出来还是张三
①现在student 表中有一条事务ID为8
的插入的数据,并且事务已提交
②有两个 事务id 分别为 10 、 20 的事务在执行
# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;
# Transaction 20
BEGIN;
# 更新了一些别的表的记录,为了确保DB_TRX_ID有值
③另外一个事务A,查询id为1的记录
BEGIN;
SELECT * FROM student WHERE id = 1; #得到的列name的值为'张三'
此刻,表student中 id 为 1 的记录的版本链就长这样,因为只有更新操作才会存在于undo log
中
解析过程
当事务A执行查询的时候,会生成如下的
ReadView
creator_trx_id(当前分配的事务ID)值为0
trx_ids(当前活跃的事务ID列表)值为[10,20]
up_limit_id(最小活跃事务ID)值为10
low_limit_id(下一个要分配的事务ID)值为21(假设该过程中没有其他事务开启)
在事务A中
根据
undo log
日志链发现,第一条事务id为10,在当前活跃事务trx_ids中,说明事务还未提交,继续向下查找,直到找到小于up_limit_id的记录,即事务ID为8的记录
,则name为张三的记录被查询出来了
④将Transaction 10
提交 Transaction 20
修改一下数据
# Transaction 10
COMMIT;
# Transaction 20
UPDATE student SET name="钱七" WHERE id=1;
UPDATE student SET name="宋八" WHERE id=1;
⑤事务A继续查询id为1的记录
SELECT * FROM student WHERE id = 1;
此刻,表student中 id 为 1 的记录的版本链长这样
解析过程
当事务A再次执行查询的时候,不会重新生成新的
ReadView
,继续使用第一次获取的ReadView
creator_trx_id(当前分配的事务ID)值为0
trx_ids(当前活跃的事务ID列表)值为[10,20]
up_limit_id(最小活跃事务ID)值为10
low_limit_id(下一个要分配的事务ID)值为21(假设该过程中没有其他事务开启)
在事务A中
根据
undo log
日志链发现,第一条事务id为20,在当前活跃事务trx_ids中,说明事务20还未提交,继续向下查找,直到找到小于up_limit_id的记录,即事务ID为8的记录
,则name为张三的记录被查询出来了