做了那么多准备工作,终于要启动 InnoDB 事务了。
作者:操盛春,爱可生技术专家,公众号『一树一溪』作者,专注于研究 MySQL 和 OceanBase 源码。
爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源。
本文基于 MySQL 8.0.32 源码,存储引擎为 InnoDB。
1. 启动事务
在《BEGIN 语句会马上启动事务吗?》这篇文章中,我们介绍过开始一个事务的 8 种 SQL 语句:
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
,表示事务还未开始。
启动事务最重要的事情之一,就是修改事务状态了,代码是这样的:
CPP
trx->state.store(TRX_STATE_ACTIVE, std::memory_order_relaxed)
事务状态从 TRX_STATE_NOT_STARTED
修改为 TRX_STATE_ACTIVE
,表示事务已经启动,是个活跃事务了。
我们执行 show engine innodb status
可能会看到类似下面的内容:
SQL
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:
CPP
trx->id = 0;
对于 ID 等于 0 的事务,查询 information_schema.innodb_trx
表得到的 trx_id 字段值并不是 0,而是一串比较长的数字:
SQL
************[ 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。我们可以通过一个例子来确认这一点:
SQL
-- 开始只读事务之前创建一个用户临时表
-- 因为只读事务里不能创建用户临时表(会报错)
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:
SQL
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
链表。这个链表记录了所有读写事务。
CPP
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 审核和管理。支持主流的开源、商业、国产数据库,为开发和运维提供流程自动化能力,提升上线效率,提高数据质量。