当前位置: 首页 > news >正文

InnoDB 事务启动探秘:从 BEGIN 到第一个 SQL,MySQL 到底在忙什么?

InnoDB 事务启动探秘:从 BEGIN 到第一个 SQL,MySQL 到底在忙什么?

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

目录

  1. 为什么需要搞清楚 BEGIN 的行为

  2. BEGIN 语句的语法变体与解析器逻辑

  3. BEGIN 的执行入口:trans_begin 函数

  4. 辞旧:隐式提交老事务与 MDL 锁释放

  5. 迎新:打上 OPTION_BEGIN 标记

  6. 事务到底什么时候真正启动?

  7. 只读事务与读写事务的差异

  8. 总结

1. 为什么需要搞清楚 BEGIN 的行为

在日常开发中,BEGIN可能是我们最常写的 SQL 之一。在很多人的认知里,BEGIN就是"开启一个事务"的意思——简单、直接、没什么好说的。

但事实真的如此吗?

如果你曾经在同一个连接里连续执行过两次BEGIN,可能会注意到一个现象:第一次BEGIN之后做了一些数据修改,还没提交,第二次BEGIN一执行,前面的修改就"消失"了。它们不是回滚了,而是被自动提交了

这说明BEGIN干的活,远比表面上看到的要多。

要真正理解BEGIN的行为,需要深入 MySQL 8.0.44 的源码,看看从客户端敲下BEGIN到事务真正可用,中间到底经历了什么。

2. BEGIN 语句的语法变体与解析器逻辑

MySQL 官方文档中,开启事务的语法比想象中要丰富:

START TRANSACTION [transaction_characteristic [, transaction_characteristic] ...] transaction_characteristic: { WITH CONSISTENT SNAPSHOT | READ WRITE | READ ONLY } BEGIN [WORK]

把这些语法要素排列组合,可以得到以下 10 种 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 /* 9 */ START TRANSACTION WITH CONSISTENT SNAPSHOT, READ WRITE, READ ONLY /* 10 */ START TRANSACTION READ WRITE, READ ONLY

其中,语句 1~8 可以正常执行,语句 9 和 10 会报语法错误。

为什么会报错?翻一下语法解析器的代码就知道了。在sql/sql_yacc.yy中,处理START TRANSACTION的逻辑是这样的:

start: START_SYM TRANSACTION_SYM opt_start_transaction_option_list { LEX *lex= Lex; lex->sql_command= SQLCOM_BEGIN; /* READ ONLY and READ WRITE are mutually exclusive. */ if (($3 & MYSQL_START_TRANS_OPT_READ_WRITE) && ($3 & MYSQL_START_TRANS_OPT_READ_ONLY)) { YYTHD->syntax_error(); MYSQL_YYABORT; } lex->start_transaction_opt= $3; } ;

解析器的逻辑很直接:**READ WRITEREAD ONLY是互斥的,不能同时出现**。一旦检测到两者同时存在,就主动报错。

在可正常执行的语句中,可以根据行为差异分为两类:

  • 语句 1~5BEGINBEGIN WORKSTART TRANSACTIONSTART TRANSACTION READ WRITESTART TRANSACTION READ ONLY):这些语句不会立即启动事务,也不会创建一致性读视图。事务的实际启动被延迟到真正需要的时候。

  • 语句 6~8(带WITH CONSISTENT SNAPSHOT的三种形式):这些语句会先启动事务,然后立即创建一致性读视图

3. BEGIN 的执行入口:trans_begin 函数

在 MySQL 源码中,所有显式开启事务的入口函数都是trans_begin。无论是BEGINBEGIN WORK还是START TRANSACTION,最终都会汇聚到这个函数中。

trans_begin函数的声明位于sql/transaction.cc中:

bool trans_begin(THD *thd, uint flags)

这个函数有一句关键的注释,直接点明了BEGIN语句的核心行为:

Beginning a transaction implicitly commits any current transaction and releases existing locks.

翻译过来就是:开始一个新事务会隐式提交当前事务并释放已有的锁

如果用四个字概括BEGIN语句在trans_begin中的核心工作,那就是辞旧迎新——先提交老事务,再准备新事务。

4. 辞旧:隐式提交老事务与 MDL 锁释放

先来看一个常见场景:

你在 MySQL 客户端中执行了BEGIN,开始了一个事务(事务 1),然后执行了一条INSERT语句。事务 1 还没有提交(处于活跃状态),此时你在同一个连接中又执行了一条BEGIN语句——事务 1 会怎样?

答案是:事务 1 会被自动提交

原因很简单:MySQL不支持嵌套事务。事务 1 还没结束,又要开始事务 2,事务 1 无处安放,只能被隐式提交。

BEGIN语句是如何判断"当前连接中是否存在未提交事务"的?源码中的判断逻辑是这样的:

if (thd->in_multi_stmt_transaction_mode() || ...) { ... }

in_multi_stmt_transaction_mode()的实现是:

inline bool in_multi_stmt_transaction_mode() const { return variables.option_bits & (OPTION_NOT_AUTOCOMMIT | OPTION_BEGIN); }

检查当前线程的option_bits标志位中,是否包含了OPTION_NOT_AUTOCOMMITOPTION_BEGIN。只要包含其中任意一个,就说明当前连接中可能存在未提交的事务。

如果检测到存在未提交事务,BEGIN语句会主动调用提交操作:

res = ha_commit_trans(thd, true);

ha_commit_trans(thd, true)的第二个参数all传的是true,表示这是一个隐式提交。这个提交不会记录在 binlog 里,也不会触发commit相关的触发器,只是悄无声息地把事务结束了。

除了提交事务,这个过程还会做一件重要的事:释放 MDL 锁。MDL(Metadata Lock)是 Server 层用来保护表结构定义的锁机制。事务持有的 MDL 锁如果不释放,后面的 DDL 语句(比如ALTER TABLE)就会被卡住。BEGIN隐式提交老事务时,会一并把 MDL 锁释放掉。

5. 迎新:打上 OPTION_BEGIN 标记

辞旧完成之后,就该迎新了。

MySQL 在资源使用上秉承一个原则:能少分配就少分配,能晚分配就晚分配。启动事务也需要分配资源,因此BEGIN语句并不会真正启动一个事务,只是做一些最轻量的准备工作。

trans_begin函数中,这个准备工作主要包括以下几行代码:

thd->variables.option_bits |= OPTION_BEGIN; thd->server_status |= SERVER_STATUS_IN_TRANS; if (thd->tx_read_only) thd->server_status |= SERVER_STATUS_IN_TRANS_READONLY;

逐行来看:

  • **thd->variables.option_bits |= OPTION_BEGIN**:给当前连接的线程打上OPTION_BEGIN标记。这是最关键的一步。

  • **thd->server_status |= SERVER_STATUS_IN_TRANS**:设置 Server 层状态,表示当前连接处于事务中。

  • **if (thd->tx_read_only) thd->server_status |= SERVER_STATUS_IN_TRANS_READONLY**:如果事务被标记为只读,则额外设置只读事务状态位。

那么,OPTION_BEGIN这个标记到底起了什么作用?

在默认情况下,MySQL 是自动提交(autocommit=1)模式,每执行完一条 SQL 语句就会自动提交。但一旦线程被打上OPTION_BEGIN标记,自动提交行为就被禁用了。事务不会在每条 SQL 之后自动提交,而是需要用户显式执行COMMITROLLBACK才会结束事务。

打个比方:你去饭店点菜。BEGIN语句就相当于你跟服务员说"我要开始点菜了",服务员在你的桌号上贴了个"用餐中"的标签。但此时厨房并没有开始做菜——真正的"做菜"(启动事务、分配事务 ID、生成 Read View 等)要等你真正点了菜(执行了具体的 SQL)才会开始。贴标签这个动作几乎不花时间,但它的意义在于告诉系统:接下来的操作都属于同一个事务,不要自动提交。

6. 事务到底什么时候真正启动?

既然BEGIN语句不会真正启动事务,那事务到底在什么时候启动?

答案是:在执行BEGIN之后的第一条 SQL 语句时

当用户执行BEGIN之后,线程只是被打上了OPTION_BEGIN标记,事务对象(trx_t)的状态仍然是TRX_STATE_NOT_STARTED,表示事务还未开始。

在 InnoDB 的源码中,事务状态定义在storage/innobase/include/trx0types.h中:

  • **TRX_STATE_NOT_STARTED**:事务对象存在,但尚未启动

  • **TRX_STATE_ACTIVE**:事务正在执行,可以获取锁和修改数据

直到执行第一条 SQL 语句(不管是SELECTUPDATE还是DELETE),InnoDB 才会真正启动事务。这个启动过程由trx_start_if_not_started()函数触发。

trx_start_if_not_started()内部会调用trx_start_low(),完成以下关键操作:

  1. 将事务状态从TRX_STATE_NOT_STARTED修改为TRX_STATE_ACTIVE

  2. 如果是读写事务,分配真正的事务 ID(trx->id

  3. 将事务对象加入全局事务链表

此时,事务才算是真正"活"了起来。我们在执行SHOW ENGINE INNODB STATUS时看到的ACTIVE状态,就来源于此。

这里还有一个值得注意的细节:事务总是以"读事务"的身份启动的。即使你执行的是UPDATE语句,事务在启动瞬间也是以读事务的身份出现,事务 ID 被设置为 0。

只有当事务确实需要写入数据时,才会被分配一个真正的事务 ID。对于只读事务,InnoDB 可以做一系列优化——不分配事务 ID、不注册到活跃事务链表、不分配 Undo 段。把"启动事务"和"分配事务 ID"解耦,可以让只读事务以极低的成本运行。

7. 只读事务与读写事务的差异

START TRANSACTION的语法中,READ ONLYREAD WRITE选项会影响事务的行为。

只读事务(START TRANSACTION READ ONLY

当以这种形式开启事务时,thd->tx_read_only会被设置为true。当 Server 层接收到任何数据更改的 SQL 时,都会直接拒绝请求,不会进入引擎层。

只读事务在 InnoDB 引擎层可以走优化过的逻辑,相比读写事务开销更小:

  • 不用分配事务 ID

  • 不用分配回滚段

  • 不用维护到全局事务链表(读写事务链表)中

这些优化使得只读事务的成本极低,非常适合只执行查询操作的场景。

读写事务(START TRANSACTION READ WRITE

这是默认的事务模式。但如果当前实例的read_only打开了,且当前连接不是超级账户,则显式开启读写事务会报错。

需要特别注意的是:**BEGIN语句默认开启的是读写事务**。即使你后续只执行SELECT查询,事务在启动时也是以读写事务的模式初始化的。不过,如果 InnoDB 在真正执行查询时能判断出这是只读操作,会进行相应的优化。

8. 总结

用一句话概括全文核心结论:**BEGIN语句并不会马上启动一个新事务**。

阶段操作事务状态关键代码
执行BEGIN① 提交老事务(如有)② 释放 MDL 锁 ③ 打上OPTION_BEGIN标记 ④ 设置SERVER_STATUS_IN_TRANSTRX_STATE_NOT_STARTED(未启动)trans_begin()ha_commit_trans()→ `option_bits
执行第一条 SQL① 调用trx_start_if_not_started()② 分配事务 ID(读写事务)③ 加入全局事务链表 ④ 状态变为TRX_STATE_ACTIVETRX_STATE_ACTIVE(已启动)trx_start_if_not_started()trx_start_low()state = TRX_STATE_ACTIVE

BEGIN语句的本质工作是"辞旧迎新"——提交可能存在的旧事务并释放其 MDL 锁,然后给当前线程打上OPTION_BEGIN标记,告诉 MySQL"接下来的操作别自动提交"。而事务的真正启动,被延迟到了第一条 SQL 语句执行的那一刻。

这种延迟启动的设计,体现了 MySQL 一贯的资源管理哲学:能不做的就不做,能晚做的就晚做。对于只执行查询的短事务来说,这种设计可以避免分配事务 ID、分配 Undo 段等不必要的开销,从而提升系统的整体吞吐量。

参考源码文件(MySQL 8.0.44)

源码文件涉及的关键函数/结构
sql/sql_yacc.yySTART TRANSACTION语法解析,READ WRITEREAD ONLY互斥检查
sql/transaction.cctrans_begin()ha_commit_trans()
sql/sql_class.hTHD结构体、in_multi_stmt_transaction_mode()option_bits标志位定义
storage/innobase/include/trx0types.htrx_state_t枚举定义(TRX_STATE_NOT_STARTEDTRX_STATE_ACTIVE等)
storage/innobase/trx/trx0trx.cctrx_start_if_not_started()trx_start_low()
http://www.jsqmd.com/news/1339866/

相关文章:

  • 热门的消防设施操作员监控证公司
  • 3分钟掌握SRWE:解锁游戏窗口分辨率调整的自由度
  • OpenRouter Auto Router 实战指南:智能路由大模型 API 实现成本与性能优化
  • Cocos Creator AI寻路参数优化:从卡顿到丝滑的调优指南
  • 2026聚焦社保高频雷区的企业用工风险老板策略课程选型指南:长三角本地化方案推荐 - 全域品牌推荐
  • 数据中台厂商怎么选?2026 年主流厂商对比与选型建议
  • 网易云音乐增强伴侣:让音乐播放体验瞬间升级的智能助手
  • 北京创业扶持机构哪家入驻流程服务省心:【博亚信诚】服务周全 - 17728181569
  • 3分钟掌握ncmdump工具:彻底解决网易云音乐NCM格式转换的完整方案
  • 构建可溯源的本地AI工作空间:基于RAG与LangChain的答案溯源实践
  • PUBG罗技鼠标宏压枪工具:5分钟快速配置终极指南
  • 智慧应急平台如何实现主动预防?三层能力闭环与数据底座解析
  • Windows热键冲突终极解决方案:5步快速定位被占用快捷键的热键侦探工具
  • JNCA投稿格式全解析:避开隐形门槛,提升录用率
  • OpenGL父子坐标转换:场景图与矩阵层级传递原理与实践
  • 佛山岩板厂家电话/佛山岩板拿货哪里便宜/佛山岩板源头厂家/佛山岩板批发找佛山集岩汇岩板/佛山工程岩板批发/佛山岩板哪家好? - GrowUME
  • Windows右键新建菜单自定义:通过注册表添加Markdown等文件类型
  • 深度解析报告厅音响内置三分频 核心原理与扩声应用 - 全域品牌推荐
  • Nginx代理WebSocket配置与优化实战指南
  • 暗黑破坏神2存档编辑器终极指南:3分钟掌握角色修改技巧
  • GBase 8a数据库容灾能力之在线备份
  • Godot资源解包实战:三步提取.pck与.exe中的脚本、纹理与音频素材
  • SteamCleaner:终极游戏缓存清理工具,3步轻松释放100GB硬盘空间
  • MySQL多版本共存配置指南与实战技巧
  • D2DX:三分钟让暗黑破坏神2焕发新生的终极高清补丁指南
  • 2026成都典当行业服务标准解析:以及时雨典当为例看合规机构四大特征
  • MCP协议:AI智能体与外部世界交互的标准化革命
  • Adobe-GenP 3.0终极破解指南:3分钟免费激活Photoshop等全系列软件
  • 2026 上海搬无忧企业搬迁夜间静音红木防护长期仓储靠谱商家参考合集 - 资讯在线
  • Elsevier投稿追踪插件:科研工作者的智能审稿助手