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:updatedelete 操作中产生的 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为张三的记录被查询出来了