MySQL InnoDB 与 Spring Boot 事务机制

内容范围 :索引与 B+Tree、数据页与 Buffer Pool、MySQL 查询执行链路、my.cnf 性能参数与作用域、MVCC 与 InnoDB 锁、Spring Boot @Transactional 原理与传播行为、NOT_SUPPORTED 专题、排障与实践。

适用基线:MySQL 8.0 / 8.4、InnoDB、Spring Boot 3.x、Spring Framework 6.x、Java 21、MyBatis / MyBatis-Plus。部分行为与特定小版本、事务管理器、驱动、SQL 执行计划有关,应结合实际环境验证。

定位:由技术讨论整理的独立学习文档。SQL / Java 片段以原理与可操作示例为主;在生产数据库执行 DDL、加锁操作或调整参数前,应先在测试环境验证并评估影响。
图表说明:本文包含 16 张可渲染的 Mermaid 流程图、时序图和结构图。建议使用支持 Mermaid 的 Markdown 编辑器(如 VS Code + Mermaid 预览扩展、Obsidian 或 GitHub)查看。纯文本查看器会显示图表源代码。

目录

  1. 总览:从一次请求到数据持久化
  2. [InnoDB 索引本质与 B+Tree 原理](#InnoDB 索引本质与 B+Tree 原理 "#2-innodb-%E7%B4%A2%E5%BC%95%E6%9C%AC%E8%B4%A8%E4%B8%8E-btree-%E5%8E%9F%E7%90%86")
  3. [InnoDB 数据页是否越大越好](#InnoDB 数据页是否越大越好 "#3-innodb-%E6%95%B0%E6%8D%AE%E9%A1%B5%E6%98%AF%E5%90%A6%E8%B6%8A%E5%A4%A7%E8%B6%8A%E5%A5%BD")
  4. [MySQL 一条 SELECT 的完整执行过程](#MySQL 一条 SELECT 的完整执行过程 "#4-mysql-%E4%B8%80%E6%9D%A1-select-%E7%9A%84%E5%AE%8C%E6%95%B4%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B")
  5. [my.cnf 中影响性能的关键参数](#my.cnf 中影响性能的关键参数 "#5-mycnf-%E4%B8%AD%E5%BD%B1%E5%93%8D%E6%80%A7%E8%83%BD%E7%9A%84%E5%85%B3%E9%94%AE%E5%8F%82%E6%95%B0")
  6. [GLOBAL、SESSION、SQL 与 TABLE 的配置范围](#GLOBAL、SESSION、SQL 与 TABLE 的配置范围 "#6-globalsessionsql-%E4%B8%8E-table-%E7%9A%84%E9%85%8D%E7%BD%AE%E8%8C%83%E5%9B%B4")
  7. [MVCC:版本链、Undo Log 与 Read View](#MVCC:版本链、Undo Log 与 Read View "#7-mvcc%E7%89%88%E6%9C%AC%E9%93%BEundo-log-%E4%B8%8E-read-view")
  8. [InnoDB 锁:S/X、记录锁、间隙锁、临键锁与死锁](#InnoDB 锁:S/X、记录锁、间隙锁、临键锁与死锁 "#8-innodb-%E9%94%81sx%E8%AE%B0%E5%BD%95%E9%94%81%E9%97%B4%E9%9A%99%E9%94%81%E4%B8%B4%E9%94%AE%E9%94%81%E4%B8%8E%E6%AD%BB%E9%94%81")
  9. [Spring Boot @Transactional 的工作原理](#Spring Boot @Transactional 的工作原理 "#9-spring-boot-transactional-%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86")
  10. 七种事务传播行为及对照
  11. NOT_SUPPORTED:外部事务"挂起"到底是什么
  12. 事务回滚、隔离级别与常见失效问题
  13. [生产排查 SQL 与 Spring 配置](#生产排查 SQL 与 Spring 配置 "#13-%E7%94%9F%E4%BA%A7%E6%8E%92%E6%9F%A5-sql-%E4%B8%8E-spring-%E9%85%8D%E7%BD%AE")
  14. 最佳实践、常见误区与学习路线
  15. 官方参考资料

1. 总览:从一次请求到数据持久化

图 1|从 HTTP 请求到 InnoDB 的完整链路

flowchart TD A["HTTP 请求"] --> B["Spring Controller / Service"] B --> C["Spring AOP 事务代理"] C --> D["MyBatis / JDBC / Connector-J"] D --> E["MySQL Server:连接与权限检查"] E --> F["SQL Parser:解析与语义处理"] F --> G["Optimizer:生成执行计划"] G --> H["Executor:执行计划"] H --> I["InnoDB:B+Tree / MVCC / 锁"] I --> J{"目标页在 Buffer Pool 中?"} J -->|是| K["读取缓存数据页"] J -->|否| L["从 SSD / 磁盘载入数据页"] L --> K K --> H H --> M["ResultSet / Java 对象"] M --> N["返回 HTTP 响应"] C -.-> O["TransactionManager:提交或回滚"] O -.-> I classDef spring fill:#e2ecff,stroke:#577eb3,color:#17365b; classDef engine fill:#e0f3e8,stroke:#4b946a,color:#195637; class A,B,C,D,M,N spring; class I,J,K,L engine;

图中为逻辑链路:查询过程中 Executor 与 InnoDB 通常会多次交互,不是仅访问一次数据页。

在 Spring Boot + MySQL 系统中,一次数据库操作可概括为:

text 复制代码
HTTP 请求
   │
   ▼
Spring Controller / Service
   │
   ├─ Spring AOP 事务代理(若 @Transactional 生效)
   │      └─ 事务管理器:获取连接、设定事务边界、提交/回滚
   ▼
MyBatis / JDBC / Connector/J
   │
   ▼
MySQL Server 层
   ├─ 连接、鉴权、解析、语义检查
   ├─ 优化器生成查询计划
   └─ 执行器执行查询计划
   │
   ▼
InnoDB 存储引擎
   ├─ B+Tree 索引查找 / 回表
   ├─ Buffer Pool / 数据页 / 磁盘 I/O
   ├─ MVCC / Undo Log / Read View
   ├─ Record / Gap / Next-Key Locks
   └─ Redo Log / 事务持久化
   │
   ▼
返回结果或提交/回滚

职责划分:Spring 负责"何时开启、参与、挂起、提交、回滚事务";InnoDB 负责"如何读写数据、锁定数据、判断版本可见性以及确保崩溃恢复"。二者不是同一个层面。


2. InnoDB 索引本质与 B+Tree 原理

2.1 为什么需要索引

假设表有 1000 万条记录:

sql 复制代码
CREATE TABLE users (
    id BIGINT PRIMARY KEY,
    username VARCHAR(100) NOT NULL,
    age INT,
    email VARCHAR(255)
) ENGINE=InnoDB;

SELECT * FROM users WHERE username = 'zhangsan';

如果 username 没有合适索引,可能需要扫描大量行并逐行判断。全表扫描工作量通常随数据量增长,近似为 O(N)。

创建索引:

sql 复制代码
CREATE INDEX idx_users_username ON users(username);

InnoDB 为索引键建立 B+Tree。搜索时按有序键从根节点一路缩小范围,而不是逐行检查所有记录。

2.2 B+Tree 的关键结构

图 2|B+Tree 三层索引结构

flowchart TB R[&#34;根索引页:分隔键&#34;] I1[&#34;内部索引页 A&#34;] I2[&#34;内部索引页 B&#34;] L1[&#34;叶子页:A~F&#34;] L2[&#34;叶子页:G~M&#34;] L3[&#34;叶子页:N~T&#34;] L4[&#34;叶子页:U~Z&#34;] R --> I1 R --> I2 I1 --> L1 I1 --> L2 I2 --> L3 I2 --> L4 L1 <-.-> L2 L2 <-.-> L3 L3 <-.-> L4 classDef internal fill:#dceaff,stroke:#638fcb,color:#1f426f; classDef leaf fill:#def4e7,stroke:#69a884,color:#1d6644; class R,I1,I2 internal; class L1,L2,L3,L4 leaf;

B+Tree 叶子页按索引键有序组织,页间有相邻页链接,方便范围扫描。图中的字母范围只用于展示分布,并非 InnoDB 的真实页格式。

text 复制代码
               [根页:分隔键]
                /        \
      [内部索引页]      [内部索引页]
         /    \             /   \
      [叶子页] [叶子页]   [叶子页] ...
          ↔       ↔           ↔
       有序的叶子页(支持范围扫描)
  • 内部节点:分隔键 + 子页面指针,负责导航。
  • 叶子节点:保存索引记录。
  • 大扇出:一个内部页可以导航到很多子页,因此树的高度通常较低。
  • 有序结构:对范围查询、按索引顺序读取、某些排序查询有优势。

理论上,B+Tree 的搜索复杂度近似为 O(log_F N) ,其中 F 是扇出。但数据库里真正重要的是:索引能否显著减少访问的数据页以及随机 I/O。

2.3 聚簇索引与二级索引

图 3|二级索引查询、回表和覆盖索引分支

flowchart TD Q[&#34;WHERE tenant_id = 1001&#34;] --> S[&#34;访问 idx_tenant 二级索引 B+Tree&#34;] S --> P[&#34;得到 tenant_id + 主键 id&#34;] P --> D{&#34;所需列都被索引覆盖?&#34;} D -->|是| C[&#34;覆盖索引:直接返回所需列&#34;] D -->|否| B[&#34;通过主键访问 PRIMARY 聚簇索引&#34;] B --> F[&#34;聚簇索引叶子记录:获取完整行&#34;] F --> O[&#34;返回 id / user_id / amount 等列&#34;] classDef yes fill:#ddf4e7,stroke:#5da97c,color:#175a37; classDef no fill:#ffeddc,stroke:#cb9458,color:#814e1a; class C yes; class B,F no;
sql 复制代码
CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    tenant_id BIGINT NOT NULL,
    user_id BIGINT NOT NULL,
    amount DECIMAL(12,2) NOT NULL,
    created_at DATETIME NOT NULL,
    KEY idx_tenant (tenant_id)
) ENGINE=InnoDB;
类型 InnoDB 叶子记录包含什么 查询特点
聚簇索引(通常为主键) 完整行记录 主键定位后可获得该行所有列
二级索引 二级索引键以及相应主键列等信息 若请求列不在索引中,需要进一步访问聚簇索引

二级索引查询示意:

text 复制代码
WHERE tenant_id = 1001
        │
        ▼
idx_tenant B+Tree 叶子页
        │  取得主键 id
        ▼
PRIMARY B+Tree 叶子页
        │  获取完整行
        ▼
返回需要的列

第二步叫回表 。若需要的列全部能从二级索引记录取得,则属于覆盖索引,通常可避免回表。

sql 复制代码
-- idx_tenant(tenant_id) 的叶子记录包含主键 id
SELECT id, tenant_id
FROM orders
WHERE tenant_id = 1001;  -- 具备覆盖索引条件

SELECT id, user_id, amount
FROM orders
WHERE tenant_id = 1001;  -- 通常还需回表

2.4 联合索引与最左前缀

sql 复制代码
CREATE INDEX idx_tenant_user_time
ON orders(tenant_id, user_id, created_at);

这个索引按 (tenant_id, user_id, created_at) 的组合顺序排列:先租户、再用户、再时间。

条件 常见索引利用情况
tenant_id = ? 可高效定位前缀
tenant_id = ? AND user_id = ? 可使用更长的等值前缀
前两列等值 + created_at 范围 可定位更窄的有序范围
仅 user_id = ? 通常不能直接利用最左前缀定位,优化器可能选择其他方法
tenant_id = ? AND created_at >= ? 可按 tenant_id 定位,但不能默认第三列都参与范围缩减

最左前缀并非"跳过首列后索引绝对无用"。索引覆盖、索引扫描、优化器选择的特殊访问方式需要区别看待。

2.5 索引未必提高速度

以下情况更可能失去索引优势:

  • LIKE '%abc' 这种前导通配符条件。
  • 对普通索引列施加函数或运算,如 YEAR(created_at)=2026。
  • 返回整表大量数据、低选择性条件、过多回表。
  • 与 ORDER BY 或 JOIN 条件不匹配的索引设计。
  • 为每列单独建索引,却没有考虑真正的高频组合查询。

原则:先观察 SQL 的谓词、返回列、排序与访问频率,再设计索引。


3. InnoDB 数据页是否越大越好

图 4|查询访问数据页时的 Buffer Pool 判定

flowchart TD A[&#34;InnoDB 需要访问某个数据页&#34;] --> B{&#34;Buffer Pool 中已有该页?&#34;} B -->|命中| C[&#34;访问内存中的页面&#34;] B -->|未命中| D[&#34;从存储设备读取整页&#34;] D --> E[&#34;将页载入 Buffer Pool&#34;] E --> C C --> F[&#34;在页内定位索引记录 / 数据记录&#34;] F --> G[&#34;向执行器提供记录&#34;] classDef hot fill:#ddf4e7,stroke:#4f9e73,color:#155b39; classDef cold fill:#fff0db,stroke:#cf9855,color:#774514; class C,E hot; class D cold;

页越大,一次缓存未命中需要搬运的数据通常越多;但相邻记录可能被一起缓存,从而提升局部性,因此不存在"越大越好"的统一结论。

InnoDB 将表和索引组织在数据库页(Page)中,默认页大小为 16KB;可选 4KB、8KB、16KB、32KB、64KB。

sql 复制代码
SHOW VARIABLES LIKE 'innodb_page_size';

3.1 小页与大页的取舍

因素 较小数据页 较大数据页
单页容量 更少 更多
B+Tree 扇出 可能较小 可能较大
B+Tree 高度 可能较高 可能较低
随机读取一条小记录 可能读取较少无关数据 可能读取较多无关数据
大范围扫描 可能需访问更多页 可能需访问更少页
Buffer Pool 缓存粒度 更细 更粗
频繁写入 取决于布局、存储与负载 取决于布局、存储与负载

关键问题不是"页是否最大",而是:

  1. 每条 SQL 访问多少个不同的数据页?
  2. 这些页是否已存在于 Buffer Pool?
  3. 查询是否具有空间/时间局部性?
  4. 读写并发、设备块大小、写放大是否更重要?

推荐:多数常规 OLTP 业务保留 16KB,除非基准测试说明其他页大小明显更好。

重要限制 :innodb_page_size 只能在初始化 MySQL 实例之前配置,已初始化实例不能直接动态改成别的页大小;不同页大小的实例不能直接混用原有 InnoDB 数据文件。


4. MySQL 一条 SELECT 的完整执行过程

图 5|SELECT 处理流水线与主要性能瓶颈

flowchart TD A[&#34;连接池取得 JDBC Connection&#34;] --> B[&#34;客户端发送 SELECT&#34;] B --> C[&#34;连接 / 认证 / 权限与元数据检查&#34;] C --> D[&#34;SQL 词法与语法解析&#34;] D --> E[&#34;语义解析:表、列、表达式&#34;] E --> F[&#34;优化器:统计信息与成本估算&#34;] F --> G[&#34;执行计划:索引扫描、JOIN、排序等&#34;] G --> H[&#34;执行器迭代执行&#34;] H --> I[&#34;InnoDB:索引 / 数据页 / MVCC&#34;] I --> H H --> J[&#34;过滤 / 聚合 / 排序 / LIMIT(按计划执行)&#34;] J --> K[&#34;MySQL 协议发送结果集&#34;] K --> L[&#34;JDBC / MyBatis 映射并返回&#34;] classDef opt fill:#dceaff,stroke:#6d93c5,color:#1b4679; classDef io fill:#f8ebd7,stroke:#c99b59,color:#744713; class F,G opt; class I io;

这里把执行算子画为教学流程;优化器可以调整物理执行顺序,某些过滤/排序在存储访问过程中便可完成或被索引消除。

例如:

sql 复制代码
SELECT id, user_id, amount
FROM orders
WHERE tenant_id = 1001
ORDER BY created_at DESC
LIMIT 10;

典型流程:

阶段 工作 主要性能关注点
1. 连接管理 应用通过 JDBC / 连接池发送 SQL 获取连接等待、网络往返
2. 解析 词法、语法解析,生成内部结构 SQL 复杂度与频繁解析
3. 语义与权限 解析表列、检查权限、类型等 解析及字典开销
4. 优化器 选择索引、访问顺序、JOIN、排序策略 行数估算、统计信息、执行计划
5. 执行器 按计划获取、过滤、聚合或排序记录 无效扫描、排序、临时表
6. InnoDB B+Tree、Buffer Pool、MVCC、锁 回表、页面未命中、长版本链
7. 返回 通过协议输出结果给客户端 结果集大小、传输、ORM 映射

不是固定的"先过滤全部行,再排序全部行":优化器可能利用索引消除排序,也可能需要临时表或额外排序。

sql 复制代码
EXPLAIN
SELECT id, user_id, amount
FROM orders
WHERE tenant_id = 1001
ORDER BY created_at DESC
LIMIT 10;

EXPLAIN ANALYZE
SELECT id, user_id, amount
FROM orders
WHERE tenant_id = 1001
ORDER BY created_at DESC
LIMIT 10;
  • EXPLAIN 展示优化器的估算与计划。
  • EXPLAIN ANALYZE 会实际执行查询,并提供运行时耗时、实际行数、循环次数等信息。
  • 重点看:key、type、rows、Extra、是否排序与回表、实际返回行数及耗时。
  • 如果 SQL 在数据库里只需 20ms,HTTP 响应却需数百毫秒,还要排查连接池排队、Java 映射、JSON 序列化和网络传输。

MySQL 8.0 不再有旧式 SQL Query Cache:Buffer Pool 缓存的是数据/索引页,而非任意 SELECT 的完整结果集。


5. my.cnf 中影响性能的关键参数

5.1 参数分类

参数 影响 主要调优原则
innodb_buffer_pool_size InnoDB 数据/索引页缓存 优先看真实读盘与内存余量
tmp_table_size 内部内存临时表大小限制 结合磁盘临时表与查询形态
temptable_max_ram TempTable 引擎全局 RAM 使用限额 关注并发复杂查询的总内存
sort_buffer_size 部分排序的会话缓冲 不能代替合适索引,不宜全局设过大
join_buffer_size 部分 JOIN 操作的缓冲 优先保证连接键索引
innodb_io_capacity / _max 后台刷脏页等 I/O 预算 根据设备能力与实际刷盘瓶颈测试
innodb_flush_method InnoDB 文件 I/O 方式 随版本/系统而异,不盲目改
innodb_redo_log_capacity Redo 日志容量及检查点压力 写入密集场景结合检查点指标
innodb_flush_log_at_trx_commit 事务提交日志持久化 优先保证数据安全
sync_binlog Binlog 持久化 根据复制和持久性要求设置
max_connections 最大连接数 与 HikariCP 总连接池规模匹配
thread_cache_size 线程复用 对频繁连接建立有效,优先考虑连接池
table_open_cache 打开表的缓存 根据表数量、Opened_tables 等观察
slow_query_log / long_query_time 慢 SQL 诊断 建议按负载开启和设置阈值

5.2 Buffer Pool 是重点,但不是越大越好

  • 可缓存聚簇索引页、二级索引页和脏页等。
  • 命中后不必再次从磁盘加载对应页。
  • 独立数据库服务器可从内存的较大比例评估,但一定预留连接、临时表、排序、系统缓存及操作系统空间;非独立 MySQL 服务器须额外为其他应用留内存。
  • 索引失配造成的大扫描,即使完全命中 Buffer Pool,仍可能浪费大量 CPU。

示例:16GB 内存、独立数据库、经过容量评估后,先测试 10GB Buffer Pool。

ini 复制代码
[mysqld]
innodb_buffer_pool_size = 10G

5.3 不要把排序/连接缓冲全部调大

ini 复制代码
[mysqld]
sort_buffer_size = 256K
join_buffer_size = 256K

这类内存可能按连接或执行操作分配;在高并发下,调得很大可能造成明显内存压力。优先寻找可支持过滤、JOIN、ORDER BY 的索引。

5.4 写入安全相关参数

ini 复制代码
[mysqld]
innodb_flush_log_at_trx_commit = 1
sync_binlog = 1

对于账号、订单、库存、支付等重要数据,通常优先重视崩溃恢复及复制可靠性。为了更高写入吞吐随意降低持久性级别可能增加故障时的数据丢失风险。

5.5 一个保守的示例(不是通用最优配置)

假设:MySQL 8.4、8 核、16GB、独立数据库服务器、SSD/NVMe、OLTP 读写混合负载。

ini 复制代码
[mysqld]
# 内存:必须结合机器真实余量核对
innodb_buffer_pool_size = 10G

# 事务可靠性
innodb_flush_log_at_trx_commit = 1
sync_binlog = 1

# 供测试的写入参数
innodb_redo_log_capacity = 1G

# 连接上限应结合应用实例与 HikariCP 总容量设计
max_connections = 200

# 不盲目调大按连接/操作使用的缓冲
sort_buffer_size = 256K
join_buffer_size = 256K

# 慢 SQL 诊断
slow_query_log = ON
long_query_time = 1
log_output = FILE

# 暂保留默认,测量后按需调整:
# innodb_io_capacity / innodb_io_capacity_max
# tmp_table_size / temptable_max_ram
# table_open_cache / thread_cache_size

参数具体默认值、是否动态和允许值随 MySQL 小版本可能不同,实际修改前用 SHOW VARIABLES 和对应版本官方手册核对。


6. GLOBAL、SESSION、SQL 与 TABLE 的配置范围

图 6|参数作用域如何决定实际生效值

flowchart TD A[&#34;需要设置一个 MySQL 参数&#34;] --> B{&#34;该参数支持什么作用域?&#34;} B -->|仅实例级| G[&#34;GLOBAL / 配置文件:整个实例生效&#34;] B -->|GLOBAL + SESSION| S[&#34;新连接继承 GLOBAL 默认值&#34;] S --> T{&#34;当前连接执行 SET SESSION?&#34;} T -->|是| U[&#34;当前连接采用 SESSION 值&#34;] T -->|否| V[&#34;当前连接保留继承的值&#34;] U --> W{&#34;本条 SQL 使用受支持的 SET_VAR?&#34;} V --> W W -->|是| X[&#34;仅本条 SQL 临时采用 SET_VAR 值&#34;] W -->|否| Y[&#34;采用当前 SESSION 值&#34;] B -->|支持 TABLE 选项| Z[&#34;ALTER TABLE:对指定表配置对应选项&#34;] classDef scope fill:#e1edff,stroke:#7295c6,color:#1d4873; classDef item fill:#def3e7,stroke:#61a57f,color:#1d6240; class G,S,Z scope; class X,Y item;

这是作用域判定示意,不是所有参数都支持四级覆盖;例如 innodb_buffer_pool_size 不能通过 SESSION 或 TABLE 调整。

误区:"全局一定优先,会覆盖表或者会话配置"。实际上不同变量支持不同作用域,只有支持相应作用域的设置才存在覆盖关系。

作用域 配置方式 举例
实例 / 全局 my.cnf、SET GLOBAL、SET PERSIST innodb_buffer_pool_size、max_connections
当前连接 / 会话 SET SESSION sort_buffer_size、join_buffer_size
当前单条 SQL /*+ SET_VAR(...) */,仅适用允许该提示的参数 对个别排序 SQL 提高 sort_buffer_size
数据表 ALTER TABLE 中支持的表选项 STATS_PERSISTENT、STATS_SAMPLE_PAGES、ROW_FORMAT

6.1 GLOBAL 与 SESSION

sql 复制代码
SHOW GLOBAL VARIABLES LIKE 'sort_buffer_size';
SHOW SESSION VARIABLES LIKE 'sort_buffer_size';

-- 更改当前连接,不影响其他会话
SET SESSION sort_buffer_size = 4194304;

-- 修改新建连接的全局默认值,不会自动替换既有会话值
SET GLOBAL sort_buffer_size = 1048576;

只对支持动态全局修改的变量可用 SET GLOBAL;持久化需要编辑配置或对允许的变量使用 SET PERSIST。

6.2 对单条 SQL 设置

sql 复制代码
SELECT /*+ SET_VAR(sort_buffer_size = 4M) */ *
FROM orders
WHERE tenant_id = 1001
ORDER BY created_at DESC;

SET_VAR 是优化器提示,只有支持的变量才生效。通过 SHOW WARNINGS 检查提示是否被忽略。

6.3 表级统计信息设置

sql 复制代码
ALTER TABLE orders
    STATS_PERSISTENT = 1,
    STATS_AUTO_RECALC = 1,
    STATS_SAMPLE_PAGES = 64;

ANALYZE TABLE orders;

表选项可覆盖相应的全局统计信息默认设置;更多统计采样不一定带来更快查询,主要改善优化器估算,可能增加采样开销。

6.4 不支持给一张表单独分配 Buffer Pool

以下写法是不存在的,不要照抄:

ini 复制代码
# 错误示意
# orders.innodb_buffer_pool_size = 8G

Buffer Pool 是 InnoDB 实例级共享缓存,不能按表划出固定专属容量。若某张表需要更好性能,优先改进索引、查询形态与热点访问;强隔离需求通常考虑拆实例。


7. MVCC:版本链、Undo Log 与 Read View

MVCC(Multi-Version Concurrency Control) 让多个事务能够在并发写入时,通过版本可见性读取符合自身快照的数据,降低读写之间的直接阻塞。

7.1 一次并发读写示例

初始:

sql 复制代码
CREATE TABLE account (
    id BIGINT PRIMARY KEY,
    balance DECIMAL(12,2) NOT NULL
) ENGINE=InnoDB;

INSERT INTO account VALUES (1, 1000.00);

事务 A:

sql 复制代码
START TRANSACTION;
UPDATE account SET balance = 800 WHERE id = 1;
-- 暂不 COMMIT

事务 B 在 RC/RR 下执行普通 SELECT:

sql 复制代码
SELECT balance FROM account WHERE id = 1;

B 通常读到可见的旧版本 1000.00,而不是等待 A 或读到未提交的 800。这就是一致性快照读的典型表现。

7.2 InnoDB 隐藏列与 Undo

图 7|当前记录与 Undo 版本链

flowchart LR V3[&#34;聚簇索引当前行 V3<br/>TRX_ID=300<br/>balance=600&#34;] -->|&#34;ROLL_PTR&#34;| U2[&#34;Undo:重建 V2<br/>TRX_ID=200<br/>balance=800&#34;] U2 -->|&#34;更早的 Undo&#34;| U1[&#34;Undo:重建 V1<br/>TRX_ID=100<br/>balance=1000&#34;] classDef current fill:#dceaff,stroke:#6d93c5,color:#1b4679; classDef historical fill:#e0f4e8,stroke:#67a784,color:#175d3b; class V3 current; class U2,U1 historical;

示意为逻辑版本链,不是表中同时存在三条完整行;Undo 提供回滚和构造旧版本所需的信息。

聚簇索引记录的隐藏元数据通常包括:

隐藏字段 用途
DB_TRX_ID 最后修改该记录的事务 ID
DB_ROLL_PTR 指向相关 Undo 记录,支持回滚/重建旧版本
DB_ROW_ID 需要 InnoDB 自动生成聚簇索引时使用的隐藏行 ID,不是每张表都需要
text 复制代码
当前行 V3 (TRX=300, balance=600)
  │  Undo 关联
  ▼
旧版本 V2 (TRX=200, balance=800)
  │
  ▼
更早版本 V1 (TRX=100, balance=1000)

这是一条逻辑版本链;不意味着每个旧版本都在业务表中保存一条完整物理行。Undo Log 提供的是恢复或重建历史版本所需的信息。

7.3 Read View 决定哪些版本可见

图 8|Read View 版本可见性判断(简化)

flowchart TD A[&#34;读取记录版本,获得其 TRX_ID&#34;] --> B{&#34;是当前事务自己的修改?&#34;} B -->|是| Y[&#34;版本可见&#34;] B -->|否| C{&#34;TRX_ID 早于 Read View 下界?&#34;} C -->|是| Y C -->|否| D{&#34;TRX_ID 达到或超过上界?&#34;} D -->|是| N[&#34;版本不可见&#34;] D -->|否| E{&#34;TRX_ID 在快照活跃集合中?&#34;} E -->|否| Y E -->|是| N N --> F{&#34;存在更早的 Undo 版本?&#34;} F -->|是| A F -->|否| Z[&#34;当前快照下无可见版本&#34;] classDef green fill:#dff5e6,stroke:#66a77d,color:#175e3c; classDef orange fill:#fff0dd,stroke:#c7955c,color:#7a4818; class Y green; class N,F orange;

这是 Read View 核心规则的教学模型;真实实现还有事务状态、版本链遍历、删除标志等细节。

Read View 保存创建快照时的活跃事务相关信息和事务 ID 边界。读取某个版本时,InnoDB 要判断修改该版本的事务是否对当前读可见;若不可见,沿 Undo 相关历史信息查找更旧的可见版本。

重要区别:

  • READ COMMITTED(RC):普通一致性读一般每条语句获取新的 Read View,因此同一事务两次 SELECT 可能看到不同已提交结果。
  • REPEATABLE READ(RR):普通一致性读通常复用事务中首次一致性读的快照,因此两次快照读的可见结果一致。
  • 默认 RR 中,BEGIN 本身通常不意味着 Read View 已立即创建;一般在首次一致性读时创建。

7.4 快照读与当前读

图 9|同一事务内的快照读与当前读

sequenceDiagram participant A as &#34;事务 A(RR)&#34; participant DB as &#34;InnoDB&#34; participant B as &#34;事务 B&#34; A->>DB: &#34;普通 SELECT:建立 Read View,读 1000&#34; B->>DB: &#34;UPDATE balance=800&#34; B->>DB: &#34;COMMIT&#34; A->>DB: &#34;普通 SELECT:沿用 Read View&#34; DB-->>A: &#34;旧可见版本 1000&#34; A->>DB: &#34;SELECT ... FOR UPDATE&#34; DB-->>A: &#34;锁定读取当前已提交版本 800&#34; Note over A,DB: &#34;假设没有其他事务继续持有冲突锁&#34;

这是简化时间线,用于区分 RR 快照读与当前读;真实锁定读结果还受事务自身修改和其他并发事务影响。

操作 机制
SELECT ...(普通查询) 一致性快照读,依据 Read View 判断可见性
SELECT ... FOR UPDATE 锁定读,获取适当的排他锁
SELECT ... FOR SHARE 锁定读,获取适当的共享锁
UPDATE / DELETE 根据当前可更新状态访问并锁定记录

容易误解的一点 :RR 下普通 SELECT 仍可能看到旧快照,但随后的 FOR UPDATE 可以看到较新的已提交版本;不能说"RR 内所有 SQL 都只见到同一个数据版本"。

7.5 长事务的代价

如果某个事务持续保持较老的 Read View,InnoDB 的 Purge 清理可能无法及时移除仍可能被读取的历史版本,导致 Undo 历史积累。即使是只读长事务,也不能认为毫无成本。


8. InnoDB 锁:S/X、记录锁、间隙锁、临键锁与死锁

MVCC 不能解决所有并发问题,修改冲突、锁定读与防止插入幻影记录仍需要锁。

8.1 锁分类

锁类型 作用
S(共享锁) 多事务可共享,但与同一资源的 X 锁冲突
X(排他锁) 保护写入/独占访问
Record Lock 锁定索引记录
Gap Lock 阻止其他事务在受保护的索引间隙插入
Next-Key Lock Record Lock + 前面的 Gap Lock
Insert Intention Lock 插入过程的间隙插入意向协调
IS / IX 表级意向锁,标识事务准备获取行级 S/X 锁
Metadata Lock(MDL) 协调表结构访问和 DDL
AUTO-INC Lock 部分自增分配场景的同步机制

S/X 兼容矩阵:

已持有 / 新请求 S X
S 兼容 冲突
X 冲突 冲突

普通 MVCC 快照读通常不需要给每条读取记录加 S 锁。

8.2 Record、Gap、Next-Key 的差别

图 10|记录锁、间隙锁、临键锁的覆盖对象

flowchart TB A[&#34;主键有序记录:10 → 20 → 30 → 40&#34;] --> R[&#34;Record Lock(20)<br/>锁住记录 20&#34;] A --> G[&#34;Gap Lock(20,30)<br/>阻止向间隙插入 25&#34;] A --> N[&#34;Next-Key Lock(20,30]<br/>锁住记录 30 和此前间隙&#34;] R --> C[&#34;适合唯一索引等值命中等场景&#34;] G --> D[&#34;主要协调间隙内的插入&#34;] N --> E[&#34;常用于 RR 范围锁定读&#34;] classDef rec fill:#dceaff,stroke:#729acb,color:#184673; classDef gap fill:#fff0d8,stroke:#d09e5d,color:#855319; classDef nk fill:#e0f4e8,stroke:#75ae8b,color:#185f3d; class R,C rec; class G,D gap; class N,E nk;

索引键:10, 20, 30, 40。

text 复制代码
Record(20)       :锁索引记录 20
Gap(20,30)       :阻止在 20 与 30 之间插入
Next-Key(20,30]  :阻止该间隙插入,并锁记录 30
  • 对已存在主键做唯一索引等值命中,往往只需记录锁。
  • RR 下的范围锁定读可能锁定扫描到的索引记录及相关间隙,以保护范围免于幻影插入。
  • 精确锁定范围取决于实际访问路径、索引、SQL 和隔离级别,不能直接把 WHERE 条件当成锁区间。
  • Gap Lock 主要阻止插入;不同事务的 Gap Lock 不必相互冲突。

8.3 可复现实验:范围锁等待

在测试库准备:

sql 复制代码
CREATE TABLE lock_demo (
    id INT PRIMARY KEY,
    amount INT NOT NULL
) ENGINE=InnoDB;

INSERT INTO lock_demo VALUES (10, 100), (20, 200), (30, 300);

连接 A:

sql 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT * FROM lock_demo
WHERE id BETWEEN 10 AND 20
FOR UPDATE;
-- 暂不提交

连接 B:

sql 复制代码
START TRANSACTION;
INSERT INTO lock_demo VALUES (15, 150);
-- 可能因 A 的锁定范围而等待

连接 A COMMIT 后,B 才可能继续。实验完记得提交或回滚两个连接。

8.4 为什么索引影响锁范围

sql 复制代码
UPDATE orders SET amount = amount + 1 WHERE tenant_id = 1001;

如果没有 tenant_id 合适索引,可能需要扫描大量索引记录,带来更大的锁竞争和更长的执行时间。建立合适索引不仅减少扫描量,还可能减少锁涉及的记录和范围。

8.5 死锁与锁等待

图 11|两个事务的循环锁等待

flowchart LR A[&#34;事务 A<br/>持有 id=10 的 X 锁&#34;] -->|&#34;等待 id=20&#34;| B[&#34;事务 B<br/>持有 id=20 的 X 锁&#34;] B -->|&#34;等待 id=10&#34;| A A -.-> D[&#34;InnoDB 死锁检测&#34;] B -.-> D D --> E[&#34;选择一个事务回滚<br/>另一事务继续&#34;] classDef tx fill:#e3edff,stroke:#6b92c3,color:#1b487a; classDef err fill:#ffe8e5,stroke:#c67b72,color:#963c35; class A,B tx; class D,E err;
text 复制代码
事务 A:锁住 id=10 → 尝试 id=20 → 等待 B
事务 B:锁住 id=20 → 尝试 id=10 → 等待 A
                       ↓
                    形成死锁
异常 常见 MySQL 错误码 默认含义
死锁 1213 InnoDB 检测到循环等待,通常回滚一个事务
锁等待超时 1205 超过锁等待阈值;默认通常只回滚当前语句

生产建议:统一多行更新顺序、缩短事务、避免事务里做慢 HTTP 调用、合理设计索引、在确认事务已回滚后重试整个事务,而非只重试其中某条 SQL。


9. Spring Boot @Transactional 的工作原理

9.1 它并不是 MySQL 事务实现本身

图 12|@Transactional 的 AOP 代理与数据库事务边界

sequenceDiagram participant C as &#34;Controller&#34; participant P as &#34;Spring AOP 代理&#34; participant TM as &#34;TransactionManager&#34; participant S as &#34;真实 Service&#34; participant DB as &#34;InnoDB&#34; C->>P: &#34;createOrder()&#34; P->>TM: &#34;getTransaction()&#34; TM->>DB: &#34;取得连接并建立事务上下文&#34; P->>S: &#34;调用目标方法&#34; S->>DB: &#34;INSERT / UPDATE(通过 Mapper)&#34; DB-->>S: &#34;SQL 执行结果&#34; S-->>P: &#34;正常返回或抛出异常&#34; alt &#34;需要回滚&#34; P->>TM: &#34;rollback()&#34; TM->>DB: &#34;ROLLBACK&#34; else &#34;允许提交&#34; P->>TM: &#34;commit()&#34; TM->>DB: &#34;COMMIT&#34; end P-->>C: &#34;返回结果或异常&#34;

对于加入外部事务的方法,commit() 并不等于此处就提交整个物理事务;最终由外层物理事务边界负责。

典型同步 JDBC 调用链:

text 复制代码
Controller
   ↓
Spring AOP Proxy
   ↓
TransactionInterceptor
   ↓
PlatformTransactionManager
   ├─ 创建/参与/挂起事务
   ├─ JDBC Connection 绑定到当前线程
   └─ commit / rollback / 清理
   ↓
OrderService 真正的方法
   ↓
MyBatis / JDBC SQL
   ↓
InnoDB

一般通过事务管理器拿到 Connection,控制自动提交和事务边界。InnoDB 继续负责 MVCC、锁、Undo、Redo。

9.2 一个典型的业务事务

java 复制代码
@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderMapper orderMapper;
    private final InventoryMapper inventoryMapper;

    @Transactional(rollbackFor = Exception.class)
    public void createOrder(Order order) {
        orderMapper.insert(order);
        int affected = inventoryMapper.decreaseStock(
            order.getProductId(), order.getQuantity());
        if (affected != 1) {
            throw new IllegalStateException("库存不足");
        }
    }
}

对应的原子条件扣减:

sql 复制代码
UPDATE inventory
SET stock = stock - :qty
WHERE product_id = :productId
  AND stock >= :qty;

:qty / :productId 是概念性绑定参数占位符;MyBatis XML 中请使用 #{qty} / #{productId} 等对应名称。affected = 1 表示扣减成功,否则应触发适当的业务失败处理。

9.3 重要注解参数

java 复制代码
@Transactional(
    propagation = Propagation.REQUIRED,
    isolation = Isolation.READ_COMMITTED,
    timeout = 10,
    readOnly = false,
    rollbackFor = Exception.class
)
public void execute() { }
参数 作用 注意
propagation 调用嵌套时采用何种事务传播行为 默认 REQUIRED
isolation 新事务的隔离级别 加入现有事务时通常沿用现有级别
timeout 事务超时设置 不等于强制中断全部 Java 代码
readOnly 只读提示 不应视为绝对写入禁令
rollbackFor 额外异常回滚规则 可覆盖默认不回滚 checked exception 的规则
noRollbackFor 指定不因某类异常触发回滚 不能强行提交已标记 rollback-only 的事务
transactionManager 指定使用哪个事务管理器 多数据源系统尤其重要

10. 七种事务传播行为及对照

图 13|传播行为选择逻辑

flowchart TD A[&#34;调用带 @Transactional 的内部方法&#34;] --> B{&#34;当前已有 Spring 事务?&#34;} B -->|是| C{&#34;传播行为&#34;} B -->|否| N{&#34;传播行为&#34;} C -->|REQUIRED / SUPPORTS / MANDATORY| J[&#34;加入现有事务&#34;] C -->|REQUIRES_NEW| K[&#34;挂起外部事务,创建独立事务&#34;] C -->|NESTED| L[&#34;在现有事务建立 Savepoint&#34;] C -->|NOT_SUPPORTED| M[&#34;挂起外部事务,非事务执行&#34;] C -->|NEVER| X[&#34;抛出异常&#34;] N -->|REQUIRED / REQUIRES_NEW / NESTED| T[&#34;创建新事务&#34;] N -->|SUPPORTS / NOT_SUPPORTED / NEVER| U[&#34;非事务执行&#34;] N -->|MANDATORY| X classDef tx fill:#e0f4e8,stroke:#66a987,color:#155f40; classDef non fill:#fff0da,stroke:#cfa063,color:#7c4d19; classDef fail fill:#ffe5e5,stroke:#ca7a7a,color:#972f2f; class J,K,L,T tx; class M,U non; class X fail;

NESTED 需要事务管理器支持保存点;已有事务时采用 NESTED 不意味着开启独立事务。

传播行为回答:"调用内部事务方法时,如果当前已经有 Spring 事务,该怎么办?"

传播行为 当前有事务 当前无事务
REQUIRED 加入现有物理事务 创建事务
REQUIRES_NEW 挂起原事务,创建独立物理事务 创建事务
NESTED 在同一物理事务里建立保存点(需支持) 创建事务
SUPPORTS 加入 不开启事务
NOT_SUPPORTED 挂起原事务,非事务执行 非事务执行
MANDATORY 必须加入 抛异常
NEVER 抛异常 非事务执行

10.1 REQUIRED:最常用

text 复制代码
OrderService @Transactional(REQUIRED)
    └─ InventoryService @Transactional(REQUIRED)
             ↓
       同一个物理事务 T1

若内部方法按回滚规则把共享事务标记为 rollback-only,外层即便捕获异常,也无法正常提交;外层提交时可能得到 UnexpectedRollbackException。

10.2 REQUIRES_NEW:独立物理事务

text 复制代码
外部 T1 ──[挂起]──► 内部 T2(新连接/独立提交或回滚)
        ◄─[恢复]──

内部事务提交后,外部后续回滚不会自动撤销内部已提交的数据。但 T2 与 T1 是两个事务,仍可能发生锁冲突:T1 持有的锁不会因为它被挂起而释放。通常还要考虑额外连接导致连接池耗尽。

10.3 NESTED:保存点

sql 复制代码
START TRANSACTION;
INSERT INTO orders ...;
SAVEPOINT item_point;
UPDATE inventory ...;
ROLLBACK TO SAVEPOINT item_point;
COMMIT;

允许在同一物理事务中对内部工作回滚到保存点。NESTED 不能独立提交;外层回滚会一起撤销内部工作。是否支持取决于事务管理器和 JDBC Savepoint 能力。


11. NOT_SUPPORTED:外部事务"挂起"到底是什么

11.1 "外部事务"究竟指什么?

外部事务 是进入被调用方法时,当前线程已经存在的 Spring 事务。它常常由外层方法上的 @Transactional 创建,但不等于外层那个注解本身;也可能来自更上层事务方法或编程式事务。

例如:

java 复制代码
@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderMapper orderMapper;
    private final LogService logService;

    @Transactional
    public void createOrder() {
        orderMapper.insertOrder();    // 属于 T1
        logService.saveLog();         // T1 暂时挂起
        orderMapper.updateOrder();    // 恢复 T1
    }
}
java 复制代码
@Service
@RequiredArgsConstructor
public class LogService {
    private final LogMapper logMapper;

    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void saveLog() {
        logMapper.insertLog(); // 不加入 T1
    }
}

为突出事务概念,示例中的 Mapper 方法简写;项目中使用实际对应的 Mapper 签名和参数。

11.2 时间线

图 14|NOT_SUPPORTED 挂起与恢复事务的时序图

sequenceDiagram participant OS as &#34;OrderService(外层)&#34; participant SP as &#34;Spring 事务代理&#34; participant DB as &#34;MySQL / InnoDB&#34; participant LS as &#34;LogService(内层)&#34; OS->>SP: &#34;进入 @Transactional createOrder()&#34; SP->>DB: &#34;建立外部事务 T1&#34; OS->>DB: &#34;INSERT orders(属于 T1,未提交)&#34; OS->>SP: &#34;调用 NOT_SUPPORTED saveLog()&#34; Note over SP,DB: &#34;挂起 T1 的 Spring 上下文;T1 未 COMMIT,已获锁仍保留&#34; SP->>LS: &#34;在非 Spring 事务上下文执行 saveLog()&#34; LS->>DB: &#34;INSERT log(通常另一连接,自动提交)&#34; DB-->>LS: &#34;日志 INSERT 完成&#34; LS-->>SP: &#34;saveLog() 返回&#34; Note over SP,DB: &#34;恢复外部事务 T1 的上下文&#34; SP-->>OS: &#34;返回 createOrder()&#34; OS->>DB: &#34;UPDATE orders(继续属于 T1)&#34; OS->>SP: &#34;createOrder() 完成或抛异常&#34; alt &#34;外层成功&#34; SP->>DB: &#34;COMMIT T1&#34; else &#34;外层需回滚&#34; SP->>DB: &#34;ROLLBACK T1;已提交日志不随之撤销&#34; end

上图刻意突出"挂起 ≠ 提交/回滚":外层事务 T1 在内层执行期间仍存在,只是不作为当前内层调用的事务上下文。

text 复制代码
T0: 进入 createOrder(),事务管理器创建 T1
T1: insertOrder() 在 T1 中执行,尚未提交
T2: 调用 saveLog(),NOT_SUPPORTED 使 Spring 挂起 T1
T3: saveLog() 不参与 T1,数据库单条 DML 通常自动提交
T4: saveLog() 返回,Spring 恢复 T1 的事务上下文
T5: updateOrder() 继续在 T1 中执行
T6: createOrder() 结束,提交或回滚 T1

"挂起"不是 COMMIT,也不是 ROLLBACK。 它只是暂时解绑外部事务的线程上下文/资源,让被调用方法在非 Spring 事务环境中执行;之后恢复原事务。外部事务原先持有的 InnoDB 锁一般仍然保持到提交或回滚。

补充 :对典型 JDBC + MyBatis 场景,saveLog() 中的 SQL 通常使用与 T1 不同的连接、处于数据库 autocommit=1 的单语句事务行为;它不是"数据库层面完全不存在事务"。

11.3 外部回滚时会发生什么?

操作 执行上下文 若操作已成功,后续 T1 回滚的影响
insertOrder() T1 回滚
saveLog() 中的单条 INSERT 非 Spring 事务,通常自动提交 通常不会被 T1 回滚
updateOrder() T1 回滚

11.4 NOT_SUPPORTED 与 REQUIRES_NEW 不同

维度 NOT_SUPPORTED REQUIRES_NEW
遇到 T1 挂起 T1 挂起 T1
是否开启内部 Spring 事务 否 是(T2)
内部两条 SQL 是否原子提交 默认不保证 按内部事务规则统一提交/回滚
外部 T1 回滚 不撤销内部已自动提交操作 不撤销内部已提交的 T2
适合的场景 允许非原子执行、不需要业务事务的工作 必须独立原子提交的一组数据库操作

如果没有外部事务,NOT_SUPPORTED 就直接以非事务方式执行,不需要挂起任何东西。

11.5 最易忽略的前提:调用必须经过 Spring 代理

java 复制代码
@Service
public class OrderService {
    @Transactional
    public void createOrder() {
        saveLog(); // this.saveLog():自调用,绕过事务代理
    }

    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void saveLog() {
        // NOT_SUPPORTED 不会因这次自调用而生效
    }
}

此时不是"外部 T1 被成功挂起",而是内部方法注解根本没有被代理拦截。请通过另一个 Spring Bean 的代理方法调用,或使用明确设计的编程式事务。


12. 事务回滚、隔离级别与常见失效问题

12.1 回滚规则

图 15|Spring 事务方法返回时的回滚决策

flowchart TD A[&#34;通过事务代理执行目标方法&#34;] --> B{&#34;目标方法是否抛出异常?&#34;} B -->|否| C{&#34;事务已被标记 rollback-only?&#34;} C -->|是| R[&#34;回滚;外层提交时可能抛出 UnexpectedRollbackException&#34;] C -->|否| S[&#34;按事务边界正常提交&#34;] B -->|是| D{&#34;异常命中 rollbackFor / noRollbackFor 等规则?&#34;} D -->|应回滚| E[&#34;回滚当前事务作用域&#34;] D -->|不应回滚| F[&#34;尝试提交当前事务作用域&#34;] E --> G[&#34;向调用方继续抛出异常&#34;] F --> G classDef green fill:#dff4e9,stroke:#66a780,color:#185d39; classDef red fill:#ffebea,stroke:#cc7975,color:#913b38; class S,F green; class R,E red;

如果内部仅参与已有物理事务,"提交/回滚当前作用域"可能体现为标记状态而非立刻提交整个数据库事务。

Spring 常见默认行为:

  • RuntimeException 与 Error:默认触发回滚。
  • 普通 checked exception(如 IOException):默认不因该异常回滚。
  • @Transactional(rollbackFor = Exception.class):扩大回滚异常类型。
  • noRollbackFor:排除指定异常,但不保证已标记 rollback-only 的事务能提交。

Spring Framework 6.2 可以配置不同的全局默认回滚策略,具体以项目版本和配置为准。

12.2 捕获异常不等于回滚

java 复制代码
@Transactional(rollbackFor = Exception.class)
public void createOrder() {
    orderMapper.insert(...);
    try {
        inventoryMapper.decrease(...);
    } catch (Exception ex) {
        log.error("failed", ex);
        // 若异常被吞掉,且事务未被标记 rollback-only,外层可能继续提交
    }
}

如果是内层经过事务代理的 REQUIRED 方法已经标记共享事务 rollback-only,则外层即使捕获异常,也可能在提交时得到 UnexpectedRollbackException。

12.3 常见事务失效/误用

情况 原因与建议
同类 this.method() 调用 绕过事务代理,独立传播设置不生效;拆分到不同 Bean
非 Spring 管理对象 new 出的对象没有 Spring 事务代理
private / CGLIB final 方法 不能按普通类代理机制正常增强;优先 public 业务方法
新线程、CompletableFuture 同步 JDBC 事务一般靠线程绑定,不自动跨线程传播
不同 DataSource 事务管理器与 Mapper 使用资源不一致,不能默认共用事务
readOnly=true 是只读提示,不是通用绝对禁写保障
timeout=10 不是自动在第 10 秒杀掉任意 Java 代码
在事务中调用外部 HTTP 持锁时间长,而且 HTTP 操作不会因 MySQL 回滚自动撤销
DDL 与 DML 混用 某些 MySQL DDL 可能隐式提交,不能假设随本地 DML 事务统一回滚

12.4 Spring 隔离设置与 InnoDB

java 复制代码
@Transactional(isolation = Isolation.READ_COMMITTED)
public void process() { }

如果这个方法创建了新物理事务 ,事务管理器可以尝试用 RC 运行;但如果它通过 REQUIRED 加入现有 RR 事务,通常使用已有事务的隔离级别,不能在同一物理事务内部随意切换。可以用 validateExistingTransaction 帮助发现配置冲突。

12.5 从业务角度防止并发覆盖

避免"先读库存,再计算并覆盖"的竞态:

sql 复制代码
-- 推荐:在数据库内一次完成条件判断与更新
UPDATE inventory
SET stock = stock - 1
WHERE product_id = 1001 AND stock > 0;

检查受影响行数,决定后续业务是否成功。若涉及多个表的一致性,由 Service 层事务统一处理;若是跨服务一致性,考虑 Outbox、幂等与补偿,不要指望一个 JDBC 事务覆盖外部系统。


13. 生产排查 SQL 与 Spring 配置

图 16|事务慢、锁等待、慢 SQL 的初步定位路径

flowchart TD A[&#34;出现接口延迟或数据库操作卡顿&#34;] --> B{&#34;先定位耗时位置&#34;} B -->|获取连接慢| C[&#34;检查 HikariCP 活跃连接 / 等待队列&#34;] B -->|SQL 执行慢| D[&#34;查看慢日志与 EXPLAIN ANALYZE&#34;] B -->|事务迟迟不结束| E[&#34;查看 innodb_trx 和长事务&#34;] D --> F{&#34;是否有锁等待?&#34;} F -->|是| G[&#34;sys.innodb_lock_waits / data_locks&#34;] F -->|否| H[&#34;排查扫描行数、索引、排序、临时表与 I/O&#34;] G --> I[&#34;确认阻塞事务、持锁 SQL 与事务时长&#34;] E --> I C --> J[&#34;检查传播行为、REQUIRES_NEW 与连接池配置&#34;] I --> K[&#34;缩短事务、统一锁顺序、针对性优化索引&#34;] H --> K J --> K classDef diag fill:#e3ecff,stroke:#7596c5,color:#1f466d; classDef fix fill:#e0f4e9,stroke:#74ad89,color:#195e41; class C,D,E,F,G,H,I,J diag; class K fix;

13.1 查看关键参数

sql 复制代码
SHOW VARIABLES WHERE Variable_name IN (
  'version',
  'transaction_isolation',
  'innodb_page_size',
  'innodb_buffer_pool_size',
  'innodb_redo_log_capacity',
  'innodb_io_capacity',
  'innodb_io_capacity_max',
  'sort_buffer_size',
  'join_buffer_size',
  'tmp_table_size',
  'temptable_max_ram',
  'max_connections'
);

13.2 Buffer Pool 读取、临时表和连接情况

sql 复制代码
SHOW GLOBAL STATUS WHERE Variable_name IN (
  'Innodb_buffer_pool_read_requests',
  'Innodb_buffer_pool_reads',
  'Innodb_buffer_pool_pages_free',
  'Created_tmp_tables',
  'Created_tmp_disk_tables',
  'Sort_merge_passes',
  'Threads_connected',
  'Threads_running',
  'Max_used_connections'
);

逻辑读命中率可以按某个时间窗口估算:

text 复制代码
HitRate ≈ 1 - (窗口内 Innodb_buffer_pool_reads 的增量
             / 窗口内 Innodb_buffer_pool_read_requests 的增量)

不能只看实例启动以来的累计总值,且高命中率不代表 SQL 没有扫描浪费。

13.3 查找耗时 SQL

sql 复制代码
SELECT
  SCHEMA_NAME,
  DIGEST_TEXT,
  COUNT_STAR,
  ROUND(AVG_TIMER_WAIT / 1000000000, 2) AS avg_ms,
  ROUND(SUM_TIMER_WAIT / 1000000000000, 2) AS total_seconds
FROM performance_schema.events_statements_summary_by_digest
WHERE DIGEST_TEXT IS NOT NULL
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;

依赖 Performance Schema 对应采集功能已启用。

13.4 查事务与锁

sql 复制代码
SELECT
  trx_id, trx_state, trx_started,
  trx_wait_started, trx_mysql_thread_id, trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started;

SELECT
  OBJECT_SCHEMA, OBJECT_NAME, INDEX_NAME,
  LOCK_TYPE, LOCK_MODE, LOCK_STATUS, LOCK_DATA
FROM performance_schema.data_locks;

SELECT
  waiting_pid, waiting_query,
  blocking_pid, blocking_query,
  wait_age_secs, locked_table, locked_index
FROM sys.innodb_lock_waits;

SHOW ENGINE INNODB STATUS;

SHOW ENGINE INNODB STATUS 中重点查看 LATEST DETECTED DEADLOCK 和 History list length。锁信息是动态快照,抓取时机很关键。

13.5 观察 Spring 事务日志

yaml 复制代码
logging:
  level:
    org.springframework.transaction: TRACE
    org.springframework.jdbc.datasource: DEBUG
    org.mybatis.spring: DEBUG

仅在需要诊断时开启较详细日志,避免长期运行导致大量日志。

可以检查当前线程的事务上下文:

java 复制代码
boolean active = TransactionSynchronizationManager
    .isActualTransactionActive();
boolean readOnly = TransactionSynchronizationManager
    .isCurrentTransactionReadOnly();

这只能证明当前存在活跃 Spring 事务上下文;不能单独证明所有 Mapper 使用的数据库连接都正确加入该事务。


14. 最佳实践、常见误区与学习路线

14.1 推荐调优顺序

  1. 确认 SQL 是否真正耗时,或慢在连接池、网络、ORM 映射。
  2. 看 EXPLAIN ANALYZE:扫描行数、索引、排序、回表、估计误差。
  3. 优化联合索引、覆盖索引、分页、返回列。
  4. 观察 Buffer Pool 命中和实际存储 I/O。
  5. 观察锁等待、长事务、死锁、Undo 历史。
  6. 最后针对指标审慎调整 my.cnf;一次只调整少量变量,并用压测确认收益。

14.2 高并发场景的事务原则

  • 事务短:尽量不要在事务中做慢网络请求、等待或复杂计算。
  • 更新顺序一致:降低交叉加锁导致的死锁风险。
  • 读写方式明确 :普通 SELECT 是 MVCC 一致性读,FOR UPDATE 是锁定读。
  • 一组数据库写入统一事务 :常用 REQUIRED。
  • 独立写入用 REQUIRES_NEW 前先评估:额外连接、锁冲突、数据一致性。
  • NOT_SUPPORTED 不等于"开一个新事务":只是让内部方法不参与已有事务。
  • 多租户数据源先确定租户再开始事务:事务绑定连接后再切换租户上下文,通常不能改变已有连接。
  • 审计/消息可靠性:只有数据库内同一个事务的操作才天然同提交或回滚;跨系统要单独设计一致性保障。

14.3 常见误区速查

误区 正确认知
索引是一个额外的数据副本 B+Tree 是有序的查找结构;二级索引记录并非完整表行
数据页越大越快 大页减少页数量,但可能增加随机访问和缓存浪费
每条索引查找必有多次磁盘 I/O 根页/内部页/叶子页可能已经缓存在 Buffer Pool
全局变量一定覆盖会话 支持 SESSION 的变量可覆盖当前连接的 GLOBAL 默认值
每张表都能单独设置 Buffer Pool Buffer Pool 是 InnoDB 实例级共享缓存
RR 事务中任何查询都只能见到同一个版本 普通快照读与锁定读语义不同
@Transactional 意味着方法里的所有动作可回滚 本地 DB 事务不自动回滚 HTTP、邮件、文件和其他服务
内层 REQUIRED 捕获异常后外层必然提交 内部可能已将共享事务标记为 rollback-only
REQUIRES_NEW 与 NESTED 等价 前者新物理事务,后者同一物理事务保存点
NOT_SUPPORTED 会结束外部事务 它只是暂时挂起并恢复,外部事务不因此提交或回滚
对内部方法直接 this 调用也能触发事务传播 默认 Spring AOP 代理模式下自调用会绕过事务拦截

14.4 建议继续深入的知识路线

text 复制代码
索引与 B+Tree
  ↓
InnoDB Page / Buffer Pool / I/O
  ↓
Optimizer / EXPLAIN ANALYZE
  ↓
MVCC / Undo / Read View
  ↓
Record / Gap / Next-Key / Deadlock
  ↓
Spring AOP / TransactionInterceptor
  ↓
Propagation / 隔离 / 回滚 / DataSource
  ↓
多租户、异步消息、Outbox、高并发事务实战

15. 官方参考资料

以下均为官方技术资料,便于进一步查证具体 MySQL / Spring 版本的细节:

MySQL

Spring 与 MyBatis


一句话总览 :MySQL 用 B+Tree 与数据页决定"数据怎么找",用 Buffer Pool 决定"能否少读磁盘",用 MVCC 与锁决定"并发时看哪个版本、谁需要等待",而 Spring @Transactional 决定"这些数据库操作如何组成一个可提交或回滚的业务事务"。

相关推荐
夕除10 小时前
Redis 过期策略
redis·笔记·面试
时间的拾荒人13 小时前
CMake 常用命令详解:从版本声明到安装部署
c++·面试
香蕉大香蕉(06敬畏市场)19 小时前
MySQL 单点详解(03)
java·数据库·mysql·面试·八股·大厂实习
道尔柯南21 小时前
C++ 内存管理详解:从 malloc/free 到 new/delete
c++·面试
CoderYanger21 小时前
Java EE 进阶:5. Spring IoC&DI(第三弹):Spring DI
java·开发语言·spring boot·spring·面试·java-ee·mvc
八月的雨季 最後的冰吻1 天前
音视频面试--延迟优化
面试·职场和发展·音视频
ShineWinsu1 天前
对于Qt:QLabel常见属性的解析
c++·qt·面试·笔试·gui·标签·qlabel
weixin_307779131 天前
RAG 级联失效分析——面试中的错误归因艺术
人工智能·python·面试·职场和发展
每天进步一点点km2 天前
自用-牛客网-华为机试编程模拟题1-python
华为od·华为·面试