likes
comments
collection
share

MySQL 核心模块揭秘 | 04 期 | 终于要启动事务了

作者站长头像
站长
· 阅读数 8

做了那么多准备工作,终于要启动 InnoDB 事务了。

MySQL 核心模块揭秘 | 04 期 | 终于要启动事务了

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

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

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

1. 启动事务

在《BEGIN 语句会马上启动事务吗?》这篇文章中,我们介绍过开始一个事务的 8 种 SQL 语句:

/* 1 */ BEGIN
/* 2 */ BEGIN WORK
/* 3 */ START TRANSACTION
/* 4 */ START TRANSACTION READ WRITE
/* 5 */ START TRANSACTION READ ONLY
/* 6 */ START TRANSACTION WITH CONSISTENT SNAPSHOT
/* 7 */ START TRANSACTION WITH CONSISTENT SNAPSHOT, READ WRITE
/* 8 */ START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY

语句 1 ~ 5 都不会马上启动新事务,只会给执行这些语句的线程打上 OPTION_BEGIN 标记,在这之后执行第一条 SQL 时,才会真正的启动事务。

在《我是一个事务,请给我一个对象》这篇文章中,我们介绍过:InnoDB 给事务分配一个对象(trx)之后,该对象的状态属性(state)值为 TRX_STATE_NOT_STARTED,表示事务还未开始。

启动事务最重要的事情之一,就是修改事务状态了,代码是这样的:

trx->state.store(TRX_STATE_ACTIVE, std::memory_order_relaxed)

事务状态从 TRX_STATE_NOT_STARTED 修改为 TRX_STATE_ACTIVE,表示事务已经启动,是个活跃事务了。

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

LIST OF TRANSACTIONS FOR EACH SESSION:
0 lock struct(s), heap size 1192, 0 row lock(s)
---TRANSACTION 206242, ACTIVE 42 sec

其中,ACTIVE 就来源于事务的 TRX_STATE_ACTIVE 状态。

2. 读事务

事务启动于执行第一条 SQL 语句时,如果第一条 SQL 语句是 select、update、delete,InnoDB 会以读事务的身份启动新事务。

读事务的 ID 会被设置为 0:

trx->id = 0;

对于 ID 等于 0 的事务,查询 information_schema.innodb_trx 表得到的 trx_id 字段值并不是 0,而是一串比较长的数字:

************[ 1. row ]************
trx_id      | 281480261177256
trx_state   | RUNNING
trx_started | 2023-12-24 22:39:45
...

上面的 trx_id 字段值是这样计算出来的:

  • 把事务对象的内存地址转换为十进制数字。
  • 用上一步得到的数字加上 281474976710656。这个数字是 6 字节能够存放的最大事务 ID + 1,6 字节是记录中隐藏的事务 ID 字段(DB_TRX_ID)占用的字节数。
  • 经过以上两步计算,就得到了 trx_id 字段值。

以上面查询出来的事务为例,事务对象的内存地址为 0x000000013afa8fa8。内存地址以 0x 开头,是十六进制,转换为十进制得到 5284466600,再加上 281474976710656 就得到了 trx_id 字段值 281480261177256

通过这个计算逻辑,我们可以根据 information_schema.innodb_trx 表中 trx_id 字段值判断事务是否分配了 ID:

  • 如果 trx_id 字段值大于等于 281474976710656,说明该事务没有分配 ID。
  • 如果 trx_id 字段值小于 281474976710656,说明该事务分配了 ID。

3. 只读事务

只读事务是读事务的一个特例,从字面上看,它是不能改变(插入、修改、删除)表中数据的。

然而,这个只读并不是绝对的,只读事务不能改变系统表、用户普通表的数据,但是可以改变用户临时表的数据。

作为读事务的特例,只读事务也要遵守读事务的规则,事务 ID 应该为 0。

只读事务操作系统表、用户普通表,只能读取表中数据,事务 ID 为 0(即不分配事务 ID)没问题。

只读事务操作用户临时表,可以改变表中数据,而用户临时表也支持事务 ACID 特性中的 3 个(ACI),这就需要分配事务 ID 了。

如果只读事务执行的第一条 SQL 语句就是插入记录到用户临时表的 insert,事务启动过程中会分配事务 ID。我们可以通过一个例子来确认这一点:

-- 开始只读事务之前创建一个用户临时表
-- 因为只读事务里不能创建用户临时表(会报错)
create temporary table t_tmp (
	id int unsigned auto_increment primary key,
  i1 int not null default 0,
  i2 int not null default 0
) engine = InnoDB default charset utf8;

-- 标识要开启一个只读事务
start transaction read only;

-- 往用户临时表中插入一条记录
insert into t_tmp(i1, i2) values (10, 100);

查询 information_schema.innodb_trx 表可以看到只读事务分配了事务 ID:

select * from information_schema.innodb_trx\G

************[ 1. row ]************
trx_id      | 206266
trx_state   | RUNNING
trx_started | 2023-12-24 21:44:51
...

trx_id 字段值 206266 小于 281474976710656,说明这个只读事务分配了事务 ID。

4. 读写事务

如果事务执行的第一条 SQL 语句是 insert,这个事务就会以读写事务的身份启动。

读写事务的启动过程,主要会做这几件事:

  • 为用户普通表分配回滚段,用于写 Undo 日志。
  • 分配事务 ID。
  • 把事务对象加入 trx_sys->rw_trx_list 链表。这个链表记录了所有读写事务。
UT_LIST_ADD_FIRST(trx_sys->rw_trx_list, trx);

5. 内部事务

用户事务以什么身份启动,取决于执行的第一条 SQL 是什么。

和用户事务不一样,InnoDB 启动内部事务都是为了改变表中数据,所以,内部事务都是读写事务

作为读写事务,所有内部事务都会加入到 trx_sys->rw_trx_list 链表中。

6. 总结

InnoDB 开启内部事务,是为了改变表中数据,所以,内部事务都以读写事务的身份启动。

用户事务可能会读取、改变表中数据,根据执行的第一条 SQL 语句不同,以不同身份启动:

  • 执行的第一条 SQL 语句是 select、update、delete,以读事务身份启动事务。
  • 执行的第一条 SQL 语句是 insert,以读写事务身份启动事务。如果只读事务执行的第一条 SQL 语句是插入记录到用户临时表的 insert,也会分配事务 ID。

本期问题:mysql_trx_list、rw_trx_list 这两个链表分别用来干什么?欢迎留言交流。

下期预告:MySQL 核心模块揭秘 | 05 期 | 读事务和只读事务的变形记。

更多技术文章,请访问:opensource.actionsky.com/

关于 SQLE

SQLE 是一款全方位的 SQL 质量管理平台,覆盖开发至生产环境的 SQL 审核和管理。支持主流的开源、商业、国产数据库,为开发和运维提供流程自动化能力,提升上线效率,提高数据质量。