爱可生开源社区 2024 全新技术专栏《MySQL 核心模块揭秘》第一期。

作者:操盛春,爱可生技术专家,公众号『一树一溪』作者,专注于研究 MySQL 和 OceanBase 源码。

爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。

本文基于 MySQL 8.0.32 源码,存储引擎为 InnoDB。

1. 事务池和管理器

作为 MySQL 中支持事务的默认存储引擎,InnoDB 对表中数据的读写操作都在事务中执行。

MySQL 被设计为支持高并发,支持很多客户端同时连接到数据库,这些连接可以同时执行 SQL。

如果这些 SQL 都要读写 InnoDB 表,InnoDB 会为每个连接启动一个事务,这意味着需要同时启动很多事务。

对于 TP 场景,通常情况下,事务都会很快执行完成。启动事务、执行 SQL、提交事务的整个流程只会持续很短的时间。

TP 是 OLTP 的简称,表示在线事务处理;与之相对的另一个常用术语 AP,是 OLAP 的简称,表示在线事务分析。

以这样一个场景为例:

  • 客户端连接到 MySQL。
  • 客户端执行 begin 语句。
  • 客户端执行一条 update 语句,按主键 ID 更新一条记录。
    这个步骤中,InnoDB 会在执行 update 语句之前,真正启动一个事务。
  • 客户端执行 commit 语句。
  • 客户端关闭数据库连接。InnoDB 会在这一步释放事务。

在这个场景下,InnoDB 事务从启动到释放的整个生命周期,有可能只持续 1 ~ 2 毫秒(甚至更短)。

由于要存放事务 ID、事务状态、Undo 日志编号、事务所属的用户线程等信息,每个事务都有一个与之对应的对象,我们称之为事务对象

每个事务对象都要占用内存,如果每启动一个事务都要为事务对象分配内存,释放事务时又要释放内存,会降低数据库性能。

为了避免频繁分配、释放内存对数据库性能产生影响,InnoDB 引入了事务池(Pool),用于管理事务。

顾名思义,事务池是一个池子,这个池子存放的东西既不是水,也不是酒,而是事务对象。

对比我们生活中的各种池子,例如:水池、洗手池、池塘,都是有大小限制的。

事务池也一样有大小限制,不能无限制的存放事务对象。数据库繁忙的时候,有很多很多事务对象,需要多个事务池来管理。

事务池多了之后,又会引发另一些问题,例如:

  • 怎么创建新的事务池?
  • 客户端创建了一个新的数据库连接,要获取一个新的事务对象,从哪个事务池获取?
  • 其它问题…

为了解决这些问题,InnoDB 又引入了事务池管理器(PoolManager),用于管理事务池。

MySQL 启动过程中,InnoDB 先创建事务池管理器,然后,事务池管理器创建并初始事务池。

2. 创建事务池管理器

InnoDB 整个生命周期中,事务池管理器只有一个,它有个很重要的属性(m_size),用于指定每个事务池能用多大内存来存放事务对象。

这个属性值来源于一个硬编码的常量值,代码里是这样定义的:

/** Size of on trx_t pool in bytes. */
static const ulint MAX_TRX_BLOCK_SIZE = 1024 * 1024 * 4;

这意味着每个事务池能用来存放事务对象的内存是 4194304 字节,也就是 4M。

MySQL 启动过程中,事务池管理器只会创建并初始化一个事务池。

这个事务池会放入事务池管理器的 m_pools 属性。这个属性是个数组(vector),用于管理所有事务池。

创建事务池的过程中,InnoDB 会分配一块 4M 的内存用于存放事务对象。

每个事务对象的大小为 992 字节,4M 内存能够存放 4194304 / 992 = 4228 个事务对象。

3. 初始化事务池

事务池创建完成之后,就该初始化了。事务池的初始化,主要是为了得到一些事务对象。

事务池有一个队列,用于存放已经初始化的事务对象。我们称这个队列为事务队列

一个事务池有 4M 内存可以存放事务对象,这块内存会被分隔成 4228 个小块。每初始化一块小内存,就会得到一个事务对象,这个事务对象会被放入事务队列。

InnoDB 初始化事务池的过程中,不会初始化全部的 4228 块小内存,只会初始化最前面的 16 块小内存,得到 16 个事务对象并放入事务队列。

初始事务池完成之后,事务队列中只有 16 个事务对象。

那么,剩余的 4212 块小内存什么时候会被初始化?

它们会在这种情况下被初始化:启动过程中初始化的 16 个事务对象都被取走使用了,事务队列变成空队列了。

此时,需要再分配一个事务对象用于启动新事务,InnoDB 就会把剩余的 4212 块小内存全部初始化,得到 4212 个事务对象并放入事务队列。

有一点需要说明,不管是启动过程中初始化的 16 块小内存,还是运行过程中初始化的 4212 块小内存,都是在循环里一个一个初始化的。每一轮循环都要干两件事:

  • 初始化一块小内存,得到一个事务对象。
  • 把事务对象放入事务池的事务队列中。

初始化小块内存的过程中,会初始化事务对象的各个属性。这里我们就不一一介绍这些属性了,等到该它们出场的时候,再按需介绍。

4. 总结

InnoDB 只有一个事务池管理器,用于管理 N 个事务池(N >= 1),每个事务池可以管理 4228 个事务对象。

MySQL 启动过程中,InnoDB 会先创建事务管理器。事务管理器会创建一个事务池,初始化 16 个事务对象放入事务池的事务队列。

MySQL 运行过程中,如果这 16 个事务对象都正在被使用,InnoDB 需要一个新的事务对象时,会一次性初始化剩余的 4212 个事务对象并放入事务池的事务队列。

本期问题:运行过程中,创建一个新的事务池,会分配多少内存?初始化多少个事务对象?
关于本期主题,如果大家有任何疑问,欢迎留言交流!

下期预告:BEGIN 语句会马上启动事务吗?