MySQL根本不支持嵌套事务,BEGIN/START TRANSACTION在已有事务中会隐式提交前一事务并开启新事务,实际仅存在单层扁平事务。

MySQL根本不支持嵌套事务
直接说结论:MySQL没有嵌套事务的概念,BEGIN / START TRANSACTION 在已存在事务的连接中,只是被忽略——它不会开启新事务,也不会报错,但后续的 COMMIT 或 ROLLBACK 仍只作用于最外层那个事务。所谓“嵌套”,本质是开发者误以为开了多个事务,实际只有一个。
常见错误现象包括:
- 内层
COMMIT后数据“看似提交”,但外层ROLLBACK仍能撤回全部操作 - 多层
try块中分别开SqlSession(如 MyBatis BATCH 模式),结果两个 session 各持一个 MySQL 连接 → 真正开了两个独立事务,而非嵌套 - 锁等待超时(
Lock wait timeout exceeded)或死锁,因为两个事务并发更新同一行,且加锁顺序不一致
MyBatis 中 SqlSession 嵌套的真实行为
当你写这样的代码:
try (SqlSession session1 = sqlSessionFactory.openSession(ExecutorType.BATCH, false)) {
mapper1.delete(...);
session1.flushStatements(); // ← 此时才真正发 SQL 到 MySQL
try (SqlSession session2 = sqlSessionFactory.openSession(ExecutorType.BATCH, false)) {
mapper2.insert(...);
session2.flushStatements(); // ← 又一次发 SQL,但走的是另一个连接
}
}这根本不是嵌套事务,而是两个独立事务(两个连接、两个事务 ID),它们在 MySQL 层面完全并行执行。问题根源在于:
-
sqlSessionFactory.openSession()每次都新建连接,不受外层 session 控制 -
ExecutorType.BATCH下,SQL 直到flushStatements()才执行,导致事务实际开始时间晚、持续时间长、锁持有时间不可控 - 若两个 session 都更新
t1中jid = 2的行,就极易触发死锁或锁等待
如何定位是不是“假嵌套”引发的问题
别猜,直接查 MySQL 实际运行状态:
- 执行
SHOW ENGINE INNODB STATUS\G,重点看LATEST DETECTED DEADLOCK区块,确认是否涉及多个TRANSACTIONID(说明是并发事务冲突,不是嵌套) - 查当前活跃事务:
SELECT * FROM information_schema.INNODB_TRX ORDER BY trx_started DESC;,观察trx_mysql_thread_id和trx_state,多个 RUNNING 状态 + 不同 thread_id = 多连接并发 - 检查错误日志:
tail -f /var/log/mysql/error.log,搜索Deadlock或Lock wait timeout,结合时间戳比对应用日志中的 session 创建时间 - 确认表引擎:
SHOW CREATE TABLE t1;,如果引擎是MyISAM,那连事务基础都没了,所有“回滚失败”都是必然
真正该做的不是模拟嵌套,而是控制事务边界
业务逻辑需要“局部回滚”?MySQL 不提供 savepoint 以外的方案(且 savepoint 不能跨连接)。正确做法是:
- 用
SAVEPOINT sp1+ROLLBACK TO sp1实现单连接内的部分回滚,但必须在同一SqlSession/ 同一 JDBCConnection内 - 避免在业务层手动 new 多个
SqlSession;统一用 Spring 的@Transactional管理外层事务,内部调用走同一个 connection - 批量操作慎用
ExecutorType.BATCH,尤其当涉及读写混合或依赖前序结果时;改用普通ExecutorType.SIMPLE,让每条 SQL 立即执行、尽快释放锁 - 如果真要隔离操作,用不同数据库或不同 schema,而不是靠“嵌套 session”假装事务隔离
最容易被忽略的一点:事务粒度和锁范围,往往比“嵌套不嵌套”更致命。一个 UPDATE 没加 WHERE 条件,或者用了 SELECT ... FOR UPDATE 却没走索引,锁住整张表,再怎么调事务结构也救不了。


















