每个事务都有一个对象,这篇文章我们聊聊,事务的对象从哪里来,要到哪里去。

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

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

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

1. 用户事务和内部事务

InnoDB 读写表中数据的操作都在事务中执行,开始一个事务的方式有两种:

  • 手动:通过 BEGINSTART TRANSACTION 语句以及它们的扩展形式开始一个事务。
  • 自动:直接执行一条 SQL 语句,InnoDB 会自动开始一个事务,SQL 语句执行完成之后,又会自动提交这个事务。

这两种方式开始的事务,都用来执行用户 SQL 语句,属于用户事务

InnoDB 有时候也需要自己执行一些 SQL 语句,为了和用户 SQL 做区分,我们把这些 SQL 称为内部 SQL。

内部 SQL 也需要在事务中执行,执行这些 SQL 的事务就是内部事务

InnoDB 有几种场景会使用内部事务,以下是其中主要的三种:

  • 如果上次关闭 MySQL 时有未提交,或者正在提交但未提交完成的事务,启动过程中,InnoDB 会把这些事务恢复为内部事务,然后提交或者回滚。
  • 后台线程执行一些操作时,需要在内部事务中执行内部 SQL。
    以 ib_dict_stats 线程为例,它计算各表、索引的统计信息之后,会使用内部事务执行内部 SQL,更新 mysql.innodb_table_stats、mysql.innodb_index_stats 表中的统计信息。
  • 为了实现原子操作,DDL 语句执行过程中,InnoDB 会使用内部事务执行内部 SQL,插入一些数据到 mysql.innodb_ddl_log 表中。

2. 分配事务对象

InnoDB 用事务池来管理事务对象,用事务池管理器来管理事务池。

不管是用户事务,还是内部事务,真正启动事务之前,都需要通过事务池管理器从某个事务池的事务队列中分配一个事务对象。

已经创建的那些事务池,都放在事务池管理器的 m_pools 数组中。分配事务对象时,先从第 1 个事务池开始,过程是这样的:

  • 如果事务池的事务队列中有可用的事务对象,直接分配一个就好了。
  • 否则,看看事务池中还有没有未初始化的小块内存。
  • 如果有,那就好办了,把这些小块内存全部初始化,得到的事务对象都放入该事务池的事务队列,并从中分配一个事务对象。
  • 否则,继续对下一个事务池,走一遍上面的流程。
  • 要是没有下一个事务池,怎么办?
  • 也好办,那就创建一个新事务池,初始化之后,就可以直接从它的事务队列中分配一个事务对象了。

3. 再做一些初始化工作

分配一个事务对象,得到的是一个出厂设置的对象,这个对象的各属性值都已经是初始状态了。

分配事务对象之后,InnoDB 还会对事务对象的几个属性再做一次初始化工作,把这几个属性再一次设置为初始值,其实就是对这些属性做了重复的赋值操作。

这些属性中,有必要提一下的是事务状态(trx->state)。出厂设置的事务对象,事务状态是 TRX_STATE_NOT_STARTED,表示事务还没有开始。

我们执行 show engine innodb status 可能会看到类似下面的内容:

LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 281480261177256, not started
0 lock struct(s), heap size 1192, 0 row lock(s)

其中,not started 就来源于事务的 TRX_STATE_NOT_STARTED 状态。

除了给几个属性重复赋值,还会改变另外两个属性的值:

  • trx->in_innodb:给这个属性值加上 TRX_FORCE_ROLLBACK_DISABLE 标志,防止这个事务被其它线程触发回滚操作。事务后续执行过程中,这个标志可能会被清除,我们就不展开介绍了。
  • trx->lock.autoinc_locks:分配一块内存空间,用于存放 autoinc 锁结构。事务执行过程中需要为 auto_increment 字段生成自增值时使用。

4. 加入事务链表

我们查询 information_schema.innodb_trx 表,能看到当前正在执行的事务有哪些,这些事务来源于两个链表。

为用户事务分配一个事务对象之后,还有一件非常重要的事,就是把事务对象放入其中一个链表的最前面,代码是这样的:

UT_LIST_ADD_FIRST(trx_sys->mysql_trx_list, trx);

从上面的代码可以看到,这个链表就是 trx_sys->mysql_trx_list,它只会记录用户事务。

至于内部事务,并不会放入 trx_sys->mysql_trx_list 链表。等到真正启动事务时,事务对象会被放入另一个链表,我们先按下不表,留个悬念,后面的内容会介绍。

5. 总结

InnoDB 把事务分为用户事务和内部事务,给事务分配对象时,会按照这个顺序:

  • 先从事务池的事务队列中分配一个对象。
  • 如果事务队列中没有可用的事务对象,就初始化事务池的剩余小块内存,从得到的事务对象中分配一个对象。
  • 如果所有事务池都没有剩余未初始化的小块内存,就创建一个新的事务池,并从中分配一个事务对象。

本期问题:InnoDB 怎么没有把内部事务也放入 trx_sys->mysql_trx_list 链表?欢迎大家留言交流。

下期预告:准备那么久,终于要启动 InnoDB 事务了。


操盛春

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