【Spring事务】Spring事务注解 @Transactional 完整体系:从 MySQL 隔离级别到 MyBatis 原理详解

发布时间:2026/7/26 18:33:57
【Spring事务】Spring事务注解 @Transactional 完整体系:从 MySQL 隔离级别到 MyBatis 原理详解 作者大家好我是 CodeStats。一个在底层技术上“考古”了四年的硬核爱好者也是 WWAIC全周项目AI编程范式的提出者和实践者。我曾手写过一个完整的 Java Web 框架从 IoC 容器到嵌入式 Tomcat代码全开源也喜欢用通俗的语言拆解 CPU、JVM、操作系统的运行本质。我的技术信条所有高深的技术最后都能用大白话讲清楚。如果讲不清楚说明还没真正理解。一、本文你将获得什么通过本文我将用“层层递进”的方式带你系统掌握 Spring 声明式事务的底层全貌MySQL事务隔离级别四种隔离级别的区别与底层实现MVCC 与锁Transactional 九个参数每个参数的作用、默认值与生产环境建议超时时间timeout到底什么时候抛异常应用层还是数据库层“加入”与“挂起”深入线程上下文彻底理解事务传播的本质MyBatis 两阶段注册 BeanSqlSessionFactory 和 MapperFactoryBean 的类结构体与职责Service 层控制 MyBatis 事务注解在 Service代理在 Mapper中间只隔着一个 ThreadLocal二、正文层层递进问题一MySQL事务四种隔离级别区别是什么本章核心总结隔离级别就是数据库在“性能”和“数据一致性”之间做权衡的四档开关。2.1.1 为什么要隔离多个事务并发执行时如果不加任何控制会出现三类数据异常异常类型表现本质脏读读到其他事务未提交的数据信任了不该信任的中间状态不可重复读同一事务两次读同一行结果不同数据被其他已提交事务修改了幻读同一事务两次范围查询记录数不同其他事务插入了新数据2.1.2 MySQL 四种隔离级别隔离级别脏读不可重复读幻读MySQL 实现方式READ UNCOMMITTED✅ 可能✅ 可能✅ 可能不加锁直接读最新版本READ COMMITTED❌✅ 可能✅ 可能MVCC每次读生成新视图REPEATABLE READ默认❌❌❌ 理论上可能MySQL 通过 MVCC 解决MVCC事务开始生成视图 间隙锁SERIALIZABLE❌❌❌读加共享锁S锁写加排他锁X锁面试追问MySQL 默认 REPEATABLE READ为什么 Oracle 默认 READ COMMITTED因为 MySQL 通过 MVCC 间隙锁在 RR 下解决了幻读但代价是更高的锁开销Oracle 的 RR 不解决幻读通过序列号快照本质上更接近 MySQL 的 RC。2.1.3 Spring 隔离级别枚举javapublic enum Isolation { DEFAULT, // 随数据库MySQLREPEATABLE_READOracleREAD_COMMITTED READ_UNCOMMITTED, READ_COMMITTED, // 生产最推荐避免脏读且性能优秀 REPEATABLE_READ, SERIALIZABLE }问题二Spring的 Transactional 注解九个参数分别什么作用本章核心总结九大参数本质是开发者在方法上贴的一张“事务执行命令清单”告诉 Spring“怎么开、怎么关、什么情况算失败”。2.2.1 参数全览参数类型默认值一句话作用value / transactionManagerString多数据源时指定用哪个事务管理器propagationPropagationREQUIRED有事务就加入没事务就新建isolationIsolationDEFAULT使用数据库默认隔离级别timeoutint秒-1无限事务最多跑多久超时就抛异常timeoutStringString-timeout 的字符串版readOnlybooleanfalse是否为只读事务优化 flushrollbackForClass[]{}哪些异常必须回滚rollbackForClassNameString[]{}同上用类名字符串noRollbackForClass[]{}哪些异常不回滚2.2.2 核心参数详解1propagation —— 7种传播行为面试高频传播行为有事务时的行为无事务时的行为经典场景REQUIRED默认加入新建增删改业务SUPPORTS加入非事务执行查询操作有也行没有更快MANDATORY加入抛异常强制要求在事务中执行REQUIRES_NEW挂起外部新建独立事务新建操作日志必须独立提交NOT_SUPPORTED挂起外部非事务执行非事务执行执行不重要的清理脚本NEVER抛异常非事务执行绝对禁止事务的场景NESTED在当前事务中创建保存点新建同REQUIRED局部回滚外部统一提交2rollbackFor —— 最关键的生产配置java// 生产环境推荐只要抛异常就回滚不管是不是RuntimeException Transactional(rollbackFor Exception.class) public void transfer() { ... }原因Spring 默认只对RuntimeException和Error回滚对SQLException、IOException等checked 异常不回滚。不写rollbackFor Exception.class数据库报错可能就悄悄吞掉了。问题三Transactional 超时时间是到时间就抛异常还是执行时检查是应用层还是数据库超时本章核心总结Transactional 的超时是“执行时检查”而非“定时炸弹”它只在数据库操作前看一眼秒表超时就报错。2.3.1 检查机制执行时检查非后台轮询答案是执行时检查。Spring 不会启动一个后台线程每秒钟去扫描超时事务而是在数据库操作执行前进行“按需检查”。检查点有三个每次获取数据库连接时最核心DataSourceUtils.getConnection()内部会检查ConnectionHolder.isTimeout()。事务提交/回滚时兜底检查防止纯逻辑执行太久。事务开始时启动一个定时任务仅用于标记超时状态配合检查点使用。2.3.2 超时计时范围Spring 事务超时 事务开始时间 到 所有数据库操作执行完成的时间。致命陷阱如果方法里有Thread.sleep(15000)纯 Java 逻辑且 timeout10不会触发超时。因为 Spring 只有在执行 SQL 时才去检查秒表sleep期间没有数据库操作不触发检查。2.3.3 应用层超时 vs 数据库超时层级实现类作用范围抛出异常应用层SpringConnectionHolder.timeout整个事务方法内的所有SQLTransactionTimedOutException数据库层JDBCStatement.setQueryTimeout()单条 SQL 语句的执行时间SQLTimeoutException两者是独立的Spring 超时到时间了会在下一次数据库操作前抛TransactionTimedOutException。数据库超时是单条 SQL 执行太久被数据库 kill 掉。问题四Transactional“加入”和“挂起”是什么意思本章核心总结“加入”就是共享同一根数据库连接“挂起”就是先把旧连接寄存起来腾出手来干自己的事。2.4.1 “加入”Join—— 共享同一连接当前线程的ThreadLocal实际是TransactionSynchronizationManager里已经有绑定的连接时新方法直接取出来用不新建。java// 加入的本质直接复用 Connection existing TransactionSynchronizationManager.getResource(dataSource); if (existing ! null) { // 这就是加入直接使用现有连接共用一个事务上下文 return existing; }后果所有加入的方法共享同一个物理连接最终一起commit或一起rollback。2.4.2 “挂起”Suspend—— 寄存旧连接换新连接当传播行为是REQUIRES_NEW或NOT_SUPPORTED时Spring 执行挂起java// 挂起的本质存旧换新 Connection old TransactionSynchronizationManager.unbindResource(dataSource); // 取出并解绑 suspendedResources old; // 存到栈里 Connection newConn dataSource.getConnection(); // 建立新连接 bindResource(dataSource, newConn); // 绑定新连接 // ... 执行内部方法 ... // 内部方法结束后恢复旧连接 bindResource(dataSource, old);后果内外两个事务完全独立内部提交/回滚不影响外部。2.4.3 REQUIRES_NEW vs NESTED终极对比维度REQUIRES_NEWNESTED连接数2个挂起旧新建新1个复用旧内部回滚内部回滚后立即提交如果成功回滚到 Savepoint不提交外部回滚不影响内部已独立提交内部一起被回滚本质两个独立物理事务一个物理事务 一个逻辑保存点问题五MyBatis 两阶段注册 Bean 完整类结构体——SqlSessionFactory 和 MapperFactoryBean 作用本章核心总结MyBatis 启动阶段就是两件事——把 SQL 编译成“蓝图”MappedStatement把 Mapper 接口包装成“工厂”MapperFactoryBean。2.5.1 第一阶段启动注册IoC 容器构建Spring Boot 启动时MybatisAutoConfiguration负责向容器注册两个核心 Beantext【阶段一BeanDefinition 注册】 1. MybatisAutoConfiguration 被 Conditional 条件触发 2. 创建 SqlSessionFactoryBean这是一个 FactoryBean └── 解析 mybatis-config.xml 和所有 Mapper.xml └── 生成 Configuration 对象内含所有 MappedStatement 3. 通过 MapperScan 扫描所有 Mapper 接口 └── 为每个 Mapper 接口生成 MapperFactoryBean 的 BeanDefinition核心类结构体java// 1. SqlSessionFactory实际实现是 DefaultSqlSessionFactory public class DefaultSqlSessionFactory implements SqlSessionFactory { // 持有核心配置对象 private final Configuration configuration; Override public SqlSession openSession() { // 从 configuration 中取 Environment获取 DataSource // 创建 Executor 和 DefaultSqlSession } } // Configuration 内部结构第一阶段解析成果 public class Configuration { // 所有 SQL 的蓝图 protected final MapString, MappedStatement mappedStatements; // 所有注册的 Mapper 接口 protected final MapClass?, MapperRegistry mapperRegistry; // 数据源、事务工厂等 protected Environment environment; }java// 2. MapperFactoryBean为每个 Mapper 接口生成代理 public class MapperFactoryBeanT extends SqlSessionDaoSupport implements FactoryBeanT { private ClassT mapperInterface; Override public T getObject() throws Exception { // 返回动态代理对象不是 Mapper 实现类 return getSqlSession().getMapper(this.mapperInterface); } }2.5.2 第二阶段运行时调用执行 SQLtext【阶段二运行时调用链】 Service 调用 mapper.selectById() ↓ MapperProxyJDK动态代理触发 invoke() ↓ MapperMethod.execute() 根据 SQL 类型路由 ↓ SqlSessionTemplateSpring包装的 SqlSession ↓ 从 Configuration 中取出 MappedStatement蓝图 ↓ Executor 执行Simple / Reuse / Batch ↓ StatementHandler 编译 SQL 设置参数 ↓ ResultSetHandler 映射结果问题六Service 层的 Transactional 如何控制 MyBatis 的事务本章核心总结Service 层通过 ThreadLocal 这个“线程公共储物柜”把连接交给 Mapper实现事务的隔空控制。2.6.1 核心答案隔空传物ThreadLocalService 和 Mapper 之间没有任何直接引用关系。Service 的代理把连接放进当前线程的ThreadLocal仓库Mapper 执行时主动去这个仓库里拿。2.6.2 完整调用链路含源码级原理步骤执行主体具体动作对 ThreadLocal 做了什么①TransactionInterceptorAOP代理进入 Service 方法前从 DataSource 获取物理连接执行setAutoCommit(false)存入TransactionSynchronizationManager.bindResource(dataSource, connection)②UserService真实业务对象执行orderDao.insert()无感不关心连接③MapperProxyJDK代理触发invoke()委托给SqlSessionTemplate无感④SqlSessionTemplate内部拦截器调用DataSourceUtils.getConnection(dataSource)取出从TransactionSynchronizationManager.getResource(dataSource)取出第①步放入的连接⑤Executor用取出的连接执行 JDBC 操作无感⑥TransactionInterceptor后置方法返回后根据是否抛异常执行commit()或rollback()移除TransactionSynchronizationManager.unbindResource(dataSource)2.6.3 为什么必须把 Transactional 加在 Service 层如果加在 Mapper 层每次调用userMapper.insert()就开启一个独立事务执行完立即提交。Service 里三个 Mapper 操作下单、扣库存、扣余额就是三个独立事务。第三个报错时前两个已经提交了无法回滚数据产生不一致。如果加在 Service 层整个 Service 方法是一个事务边界。所有 Mapper 操作共享同一个连接一起提交或一起回滚。保证业务原子性。2.6.4 Spring MyBatis 整合的连接获取源码级逻辑java// SqlSessionTemplate 内部获取连接的核心逻辑简化 public Connection getConnection(DataSource dataSource) { // 1. 先去当前线程的 ThreadLocal 里找事务管理器放连接的地方 ConnectionHolder holder TransactionSynchronizationManager.getResource(dataSource); if (holder ! null holder.hasConnection()) { // 2. 找到了说明当前方法被 Transactional 包裹 // 直接返回这个已经开启事务的连接这就是“加入”当前事务 return holder.getConnection(); } else { // 3. 没找到说明没有事务 // 直接从数据源 new 一个物理连接非事务场景用完立即关闭 return dataSource.getConnection(); } }三、全文内容总结层层递进的逻辑链条我们把六个问题串起来形成一条完整的技术认知链层级核心问题核心结论数据库层问题一MySQL 隔离级别怎么选隔离级别是“性能”与“一致性”的四档开关READ_COMMITTED是生产最佳平衡点。Spring API 层问题二九个参数怎么配九大参数是贴在方法上的“事务执行命令清单”rollbackForException.class是必加项。Spring 内部机制问题三、四超时和传播怎么实现超时是“执行时检查”而非定时炸弹加入/挂起本质是 ThreadLocal 中连接的“共享”与“寄存”。MyBatis 整合层问题五MyBatis 怎么注册到 Spring启动阶段把 SQL 编译成MappedStatement蓝图把 Mapper 包装成MapperFactoryBean工厂。运行时线程层问题六Service 怎么控制 Mapper通过TransactionSynchronizationManagerThreadLocal 仓库完成连接的隔空传递与边界控制。终极结论Spring 事务的本质从来不是代码块嵌套而是数据库连接Connection在不同层级之间有序流转与边界控制。理解了ThreadLocal这个“线程公共储物柜”你就理解了 Spring 事务、MyBatis 整合、乃至整个 Java Web 声明式治理的底层灵魂。四、最后如果这篇从数据库隔离级别一路“考古”到 ThreadLocal 底层流转的文章对你有帮助欢迎点赞 —— 让更多朋友看到硬核内容收藏⭐ —— 随时回顾 Spring MyBatis 事务全链路关注 —— 下次我讲“手写 Spring 事务管理器实现”时第一时间收到有任何疑问欢迎评论区交流我会从源码层面一一回复。