共计 3904 个字符,预计需要花费 10 分钟才能阅读完成。
MySQL 8.0:InnoDB 中大对象的 MVCC
在本文中,我将解释 MySQL InnoDB 存储引擎中大对象(LOB)设计的多版本并发控制(MVCC)。MySQL 8.0 有一个新功能,允许用户部分更新大型对象,包括 JSON 文档。使用此部分更新功能,当 LOB 部分更新时,MVCC 对 LOB 的工作方式已发生变化。对于正常更新(完整更新),MVCC 将像以前的版本一样工作。让我们看一下 MVCC 在不涉及部分更新时的工作原理,然后考虑对 LOB 进行部分更新的用例。
MVCC 常规更新
我使用术语常规更新来指代不是部分更新的更新。我将通过一个例子解释 MVCC 如何用于常规更新大对象。我将为此目的使用以下 mtr(1) 测试用例:
create table t1 (f1 int primary key , f2 longblob) engine = innodb ;
insert into t1 values (1 , repeat ( ‘a’ , 65536) ) ;
start transaction ;
update t1 set f2 = repeat (‘b’ , 65536) where f1 = 1 ;
— echo # Connection con1:
— 对于使用 MySQL 客户端的用户,可能需要通过另开一个终端窗口建立新链接,下同。
connect (con1 , localhost , root , ,) ;
— echo # Must see the old value ‘aaaaaaaaaa’
select f1 , right (f2 , 10) from t1 order by f1 ;
— echo # Connection default:
connection default ;
disconnect con1 ;
commit ;
drop table t1 ;
为了理解下面的解释,仔细理解上述测试用例非常重要。
测试场景如下:
最初,表 t1 包含单个记录(R1)。
事务 trx1 将记录更新为新值。
当 trx1 仍处于活动状态时,另一个事务 trx2 正在读取记录。它将读取旧值。
表 t1 仅包含一个记录(R1)。但是 trx1 和 trx2 会看到两个不同的值。该表实际上只包含最新值(trx1 所见的值),而 trx2 看到的值或记录是从撤消日志记录中获得的。让我们看下面的图片来更好地理解它。
初始状态:更新操作之前
下图显示了更新操作之前的情况。撤消日志为空。表的聚簇索引包含一行。表中有一个 LOB。聚簇索引记录包含对 LOB 的引用。
最终状态:更新操作后
现在让我们看一下更新操作后的情况。
以下是一些重要的观察:
用户表空间中有两个 LOB – 旧的 LOB 和新的 LOB。旧的 LOB 只能通过撤消日志访问。聚集索引记录指向新 LOB。
更新操作已创建包含更新向量的撤消日志记录。此撤消日志记录指向旧 LOB。
聚簇索引记录通过 DB_ROLL_PTR 系统列指向撤消日志记录。此滚动指针指向撤消日志记录,该记录可用于构建聚簇索引记录的先前版本。
撤消记录不包含 LOB 本身。而是它只包含对存储在用户表空间中的 LOB 的引用。
存储在撤消日志记录中的 LOB 引用与存储在聚簇索引记录中的 LOB 引用不同。
事务在连接 1 中采取的步骤如下:
事务查看 R1 并确定尚未提交修改聚簇索引记录的事务。这意味着它无法读取该记录(因为默认隔离级别是 REPEATABLE READ)。
它查看 R1 中的 DB_ROLL_PTR 并找到撤消日志记录。使用撤消日志记录构建 R1 的先前版本。
它读取了这个构建的旧版 R1。请注意,此版本在聚簇索引记录中不可用。但它使用撤消记录即时构建。
当 R1 指向新的 LOB 时,这个构造的旧版本的 R1 指向旧的 LOB。所以结果包含旧的 LOB。
这是 LOB 的 MVCC 在不涉及部分更新时的工作方式。
MVCC 部分更新
让我们看另一个例子,了解 MVCC 在部分更新的情况下是如何工作的。我们需要另一个例子,因为目前仅通过函数 json_set()和 json_replace()支持 JSON 文档的部分更新。
create table t2 (f1 int primary key , j json) engine = InnoDB ;
set @ elem_a = concat (‘”‘ , repeat ( ‘a’ , 200) , ‘”‘ ) ;
set @ elem_a_with_coma = concat (@ elem_a , ‘,’) ;
set @ json_doc = concat (“[” , repeat ( @ elem_a_with_coma , 300) , @ elem_a , “]” ) ;
insert into t2 (f1 , j) values (1 , @ json_doc) ;
start transaction ;
update t2 set j = json_set (j , ‘$[200]’ , repeat (‘b’ , 200) ) where f1 = 1 ;
— echo # Connection con1:
connect (con1 , localhost , root , ,) ;
— echo # Must see the old value ‘aaaaaaaaaa…’
select json_extract (j , ‘$[200]’ ) from t2 ;
— echo # Connection default:
connection default ;
disconnect con1 ;
commit ;
该场景与前面的示例相同。只是 longblob 字段已更改为 JSON 文档。加载的数据也略有不同,以符合 JSON 格式。
提示:您可以在上述 mtr 测试用例(两者中)中添加语句 set debug =’+ d,innodb_lob_print’,以在服务器日志文件中打印 LOB 索引。LOB 索引将在插入后立即打印。LOB 索引将为您提供存储的 LOB 对象的结构。在部分更新操作之前
完全或部分更新操作之前的初始条件是相同的,并且已经在上面给出。但是在下图中,提供了一些附加信息。
让我们看看图中显示的其他信息:
存储在聚簇索引记录中的 LOB 引用现在包含 LOB 版本号 v1。在初始插入操作期间,将其设置为 1,并在每次部分更新时递增。
每个 LOB 数据页面在 LOB 索引中都有一个条目。每个条目都包含 LOB 版本信息。每当修改一个 LOB 数据页时,它将被复制到具有新数据的新 LOB 数据页中,并且将创建具有递增的 LOB 版本号的新 LOB 索引条目。
附加信息是 LOB 版本号。这在聚集索引记录中的 LOB 引用中以及 LOB 索引的每个条目中都可用。部分更新操作后
下图说明了部分更新操作后的情况。
这里最重要的优化是用户表空间中仍然只有一个 LOB。仅更新需要修改的那些 LOB 数据页。部分更新操作后的这个单个 LOB 包含旧版本和新版本的 LOB。图中 LOB 数据页面上的 v1 和 v2 标签说明了这一点。
另一个重要的观察是撤消日志和聚簇索引记录中的 LOB 引用指向同一个 LOB。但 LOB 引用包含不同的版本号。撤消日志记录中的 LOB 引用包含 v1(旧版本号),聚簇索引记录中的 LOB 引用包含新版本号 v2。
LOB 版本号的目的
如上所示,具有不同版本号的不同 LOB 引用指向相同的 LOB。单个 LOB 包含来自不同版本的部分。LOB 版本号用于获取各种 LOB 引用指向的正确版本。在本节中,我们将了解如何完成此操作。
LOB 索引包含组成 LOB 的 LOB 页面列表。它包含 LOB 数据页的页码,每个 LOB 数据页包含的数据量以及版本号。此列表的每个节点称为 LOB 索引条目。每个 LOB 索引条目都包含旧版本的列表。让我们看一个说明上述部分更新测试用例的结构的图。
最初,在完成部分更新之前,LOB 索引总共包含 4 个条目。四个条目的页码是 5,6,7 和 8. 没有 LOB 索引条目具有旧版本。所有四个条目的版本号均为 1。
部分更新完成后,我们注意到页码 9 已替换页码 7,页码 7 现在被视为页码 9 的旧版本。页码 9 的版本号为 2,并且页码 7 的版本号为 1。
部分更新完成后,当通过版本号为 1 的 LOB 引用访问 LOB 时,将查看第 5 页的第一个索引条目。它的版本号为 1. 如果索引条目中的版本号小于或等于 LOB 引用中的版本号,则将读取该条目。因此,将读取第 5 页。然后将查看页码为 6 的索引条目。它的版本号为 1,因此将被读取。然后将查看页码为 9 的索引条目。它的版本号为 2. 但是 lob 引用的版本号为 1. 如果索引条目中的版本号大于 LOB 引用中的版本号,则不会读取该条目。由于页码 9 的条目具有版本 2,因此将查看其旧版本。将检查页码为 7 的索引条目。它的版本号为 1,因此将被读取。在此之后,将检查页码为 8 的索引条目。它的版本号为 1,因此也将被读取。这是访问旧版 LOB 的方式。
部分更新完成后,当通过版本号为 2 的 LOB 引用访问 LOB 时,将查看第 5 页的第一个索引条目。它的版本号为 1. 如果索引条目中的版本号小于或等于 LOB 引用中的版本号,则将读取该条目。因此它将按顺序读取页码 5,6,9,8。由于版本号始终 <= 2,因此无需使用旧版本访问页码 7。
需要记住的一点是 LOB 在 InnoDB 中不是独立存在的。它被视为聚簇索引记录的扩展。LOB 对事务是否可见并不由 LOB 模块处理。LOB 模块只是处理聚簇索引记录。如果事务访问 LOB,则意味着它已经在聚簇索引记录中的 DB_TRX_ID 的帮助下确定它可以查看 LOB(而不是 LOB 的特定版本)。所以我们不担心 LOB 模块中的那个方面。我们只专注于为给定的 LOB 版本号提供正确的内容。
结论
在本文中,我们了解了如何在 InnoDB 中为大对象完成 MVCC。当对 LOB 进行部分更新时,多个 LOB 引用可以指向同一个 LOB。但他们将拥有不同的版本号。使用这些 LOB 版本号,可以访问正确的 LOB 内容。
希望您发现此信息有用。
谢谢你使用 MySQL!
注释:(1) Mtr 即 Mini-transaction 的缩写,字面意思小事物,相对逻辑事物而言,我们把它称作物理事物。属于 Innodb 存储引擎的底层模块。主要用于锁和日志信息。