
1. 从一次“数据错乱”聊起为什么必须理解Spring事务事情是这样的。某天我接手了一个老项目里面有一段保存订单的逻辑先往订单表插一条记录再往库存表扣减数量最后往日志表写一条操作记录。三个操作顺序执行看起来没什么问题。上线第二天就被用户投诉了——下单成功但库存没扣或者订单没生成但库存已经扣了。排查了半天原因很简单三个数据库操作分属三个独立的事务中间任何一个抛了异常前面的操作已经提交了根本不会回滚。这就是典型的“没有事务管理”的后果。很多刚接触Spring的同学觉得事务就是加一个Transactional注解事务管理概述这节没什么好学的。但真到了排查线上问题、设计复杂业务接口的时候如果对Spring事务的底层机制不清楚你连问题出在哪都定位不了。这篇博文把Spring事务管理的基础概念讲透先从数据库事务的本源出发说明ACID、隔离级别这些绕不开的基础再把Spring事务管理的整体设计思路拆开讲清楚三个核心接口PlatformTransactionManager、TransactionDefinition、TransactionStatus各自负责什么以及它们之间怎么配合最后结合常见的DataSource事务管理器聊聊实际开发中你可能遇到但文档里不会明说的问题。这个内容适合谁看正在学Spring框架的初学者可以把这篇当成一个体系化的入门索引面试前准备事务知识点的同学能从中找到清晰的答题框架已经用Transactional但没深究过原理的开发者也值得花几分钟把概念重新捋一遍。毕竟事务管理在Spring里不是孤立的知识点它牵扯到AOP代理、异常处理机制、数据源连接管理牵一发动全身。2. 先回到数据库层面事务到底是什么2.1 一个生活类比把事务讲明白我特别喜欢用一个例子来解释事务转账。A账户给B账户转100块钱这件事在数据库里至少拆成两步——先从A账户扣100再往B账户加100。如果第一步执行成功、第二步执行到一半数据库崩了会出现什么情况A的钱没了B的钱没到账。谁也不希望这种事发生在自己身上。事务就是用来解决这个问题的机制。把“扣钱”和“加钱”两个操作绑定成一个整体这个整体要么全部执行成功要么全部不执行。数据库通过事务保证了一组SQL操作的一致性不会出现“执行了一半”这种中间状态。这个类比在面试里也特别好用。面试官问“什么是事务”你用转账场景一讲比干巴巴背定义生动得多也说明你真的理解事务解决了什么问题而不只是记住了概念。2.2 ACID四要素每个都要能展开说事务的四个核心特性——原子性Atomicity、一致性Consistency、隔离性Isolation、持久性Durability——合称ACID。这四个概念每个都能单独拿出来问千万别只是背单词。原子性说的是事务里的所有操作要么全成功要么全不成功不存在中间态。实现原子性的核心机制是undo logMySQL的InnoDB引擎在事务执行过程中会把数据变更前的镜像记录到undo log里一旦需要回滚就用这些镜像把数据恢复原样。一致性指的是事务执行前后数据都要满足业务上的约束规则。比如转账前后A加B的总金额不能变账户余额不能为负。这个特性最容易被忽略因为它不完全靠数据库保证更多时候靠业务代码逻辑正确。数据库层面能保证的约束是外键、唯一索引、非空这些硬规则但“总金额不变”这种业务约束得靠应用层配合。隔离性解决的是多个事务并发执行时的相互影响问题。数据库提供了多版本并发控制MVCC和锁机制来控制隔离程度这也是下一节要展开的重点。持久性最容易理解事务一旦提交数据的修改就是永久的哪怕系统立刻宕机数据也不会丢。InnoDB通过redo log实现持久性提交事务时先把redo log刷到磁盘再更新内存中的数据页这样即使崩溃恢复时也能通过redo log重放操作。2.3 隔离级别并发场景下绕不开的四个档位隔离级别这块我建议把所有开发者都记住一句话隔离级别越高数据越安全但并发性能越差。四个级别的对比用大白话说是这样的隔离级别脏读不可重复读幻读一句话理解READ UNCOMMITTED可能可能可能别人没提交的数据我也能读到基本裸奔READ COMMITTED不可能可能可能只能读已提交的数据大多数数据库的默认级别REPEATABLE READ不可能不可能可能InnoDB可避免同一个事务里多次读取结果一致SERIALIZABLE不可能不可能不可能事务完全串行执行安全但慢脏读就是读到了别人事务还没提交的数据别人一Rollback你手里的数据就是脏的。不可重复读是在同一个事务里两次执行同样的SQL查出来的数据不一样——因为别的并发事务在这期间修改并提交了数据。幻读更微妙两次查询返回的结果集行数不一样像是出现了幻觉数据。MySQL InnoDB引擎默认使用REPEATABLE READ但通过间隙锁Gap Lock和临键锁Next-Key Lock机制在多数场景下连幻读也能避免。这是MySQL一个比较特别的地方。Oracle默认是READ COMMITTED如果你在不同数据库之间切换过会发现同一个事务隔离级别的表现并不完全一样这事必须心里有数。3. Spring为何要自己造一套事务抽象3.1 直接使用数据库事务API的痛点如果你在纯JDBC时代写过代码应该对这段流程有印象注册驱动、获取Connection、setAutoCommit(false)、执行SQL、commit()或rollback()、finally里close()释放资源。一套操作走下来事务控制代码和业务代码完全耦合在一起到处都是样板代码。更麻烦的是事务控制方式和具体技术栈绑定。你是用JDBC控制事务还是用Hibernate的Session控制事务你的事务管理器是DataSourceTransactionManager还是JpaTransactionManager如果你的业务代码里直接依赖了具体的连接获取和提交回滚逻辑将来要切换持久层技术方案就等于把所有业务代码重写一遍。Spring事务管理要解决的正是两个核心问题第一把事务控制的样板代码从业务逻辑中抽离出去第二在不同事务管理API之上建立统一抽象让业务代码不感知底层用的是JDBC、Hibernate还是其他持久化框架。3.2 声明式事务与编程式事务怎么选Spring提供了两种事务管理方式编程式事务和声明式事务。编程式事务就是在业务代码里显式调用事务管理器的API来控制事务边界代码长这样Autowired private PlatformTransactionManager transactionManager; public void doSomething() { DefaultTransactionDefinition def new DefaultTransactionDefinition(); TransactionStatus status transactionManager.getTransaction(def); try { // 业务操作 transactionManager.commit(status); } catch (Exception e) { transactionManager.rollback(status); throw e; } }声明式事务就是大家最熟悉的Transactional注解方式Spring通过AOP在目标方法执行前后自动开启、提交或回滚事务业务代码里完全看不到事务管理逻辑Transactional public void createOrder(Order order) { orderDao.insert(order); stockDao.decrement(order.getProductId(), order.getCount()); }我的建议是新项目能用声明式事务就用声明式事务它让代码最干净、侵入性最低。但编程式事务并没有被淘汰反而在那些需要精细控制事务边界的场景里很有用——比如你只想让某个方法内的部分操作参与事务又或者你需要在同一个方法里动态决定某些操作是否开启独立事务。两种方式都值得掌握实际开发中都有用武之地。3.3 设计核心把“做什么”和“怎么做”分离Spring事务管理的整体设计思路可以归结为一句话业务代码只声明事务需求传播行为、隔离级别、超时时间等事务管理器负责跟具体的数据源技术打交道完成真正的开启、提交、回滚动作。这个设计借鉴了策略模式的思想。业务开发者在Transactional注解里声明“我要一个只读事务隔离级别是读已提交”这是“做什么”的问题。至于这个事务具体怎么在MySQL上开启、锁怎么加、什么时候刷redo log那是事务管理器的事跟业务代码无关。理解了这个设计你再看PlatformTransactionManager、TransactionDefinition、TransactionStatus这三个接口就一点也不难了一个管“事务生命周期操作”一个管“事务该怎么定义”一个管“当前事务运行到哪一步了”。4. 三大核心接口逐个拆解4.1 PlatformTransactionManager事务操作的总指挥PlatformTransactionManager是Spring事务管理的顶级接口也是整个事务抽象体系里最核心的一个接口。它定义了三个方法public interface PlatformTransactionManager { TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException; void commit(TransactionStatus status) throws TransactionException; void rollback(TransactionStatus status) throws TransactionException; }getTransaction负责获取一个事务。注意它的入参是TransactionDefinition返回的是TransactionStatus。这里有一个关键点getTransaction并不一定总是开启新事务它要根据TransactionDefinition里声明的传播行为来决定——是加入当前事务、挂起当前事务再开新事务还是直接抛异常。这个机制后面讲传播行为时会详细展开。commit和rollback分别负责提交和回滚事务。Spring里的回滚逻辑有一点容易踩坑默认情况下只有运行时异常RuntimeException和错误Error才会触发回滚受检异常CheckedException默认不会回滚。后面第6节会专门讲这个问题。还有一点值得注意PlatformTransactionManager是一个抽象接口它在执行commit时会根据TransactionStatus里的rollback-only标记判断——如果当前事务已经被标记为只能回滚即使你主动调用commit最终执行的动作也还是回滚。这是Spring事务里一个隐蔽但重要的行为。4.2 TransactionDefinition事务该怎么开由它说了算TransactionDefinition是事务属性的定义接口决定了事务的“行为方式”。打开这个接口里面定义的方法并不多但每一个都对应一类经典面试题。public interface TransactionDefinition { int getPropagationBehavior(); // 传播行为 int getIsolationLevel(); // 隔离级别 int getTimeout(); // 超时时间秒默认-1不超时 boolean isReadOnly(); // 是否只读事务 String getName(); // 事务名称 }传播行为propagation behavior是在多个事务方法相互调用时事务该如何传播的规则。Spring定义了七种传播行为实际开发中用的最多的是REQUIRED和REQUIRES_NEW。REQUIRED是默认值意思是如果当前存在事务就加入不存在就新建REQUIRES_NEW则是无论如何都挂起当前事务、开启一个新事务。这里我用自己的经验提醒一句REQUIRES_NEW虽然好用但一定要慎重。它意味着内部方法的事务不会跟随外部事务一起回滚这个特性在某些场景下是救命稻草——比如你希望操作日志无论主流程成功失败都记录下来——但滥用它会导致数据一致性出现意想不到的问题比如主事务回滚了子事务的数据却留下了。隔离级别在接口里以常量形式存在取值分别是ISOLATION_DEFAULT使用数据库默认、ISOLATION_READ_UNCOMMITTED、ISOLATION_READ_COMMITTED、ISOLATION_REPEATABLE_READ、ISOLATION_SERIALIZABLE。默认值ISOLATION_DEFAULT表示采用底层数据库的默认隔离级别这也是我推荐大家使用的默认值业务代码里没必要手动指定——除非你明确知道自己在做什么特殊场景。isReadOnly只读事务也是一个高频考点。只读事务并不是强制数据库端只读它是一个性能优化提示像Hibernate这类框架在只读事务下会关闭脏检查机制减少不必要的快照维护开销。一个查询密集的方法加上Transactional(readOnly true)多数情况下确实会有性能改善。但千万记住只读事务里执行写操作在某些数据库上并不会报错而是“静默失败”这种问题排查起来特别费劲。getTimeout超时时间默认是-1即使用底层数据库的默认事务超时设置。如果设置了具体秒数事务执行超过这个时间会被强制回滚。实际开发里长事务会占用数据库连接、锁住数据行所以给那些可能执行很久的事务加上超时限制是个好习惯。4.3 TransactionStatus当前事务的状态观察员与控制器TransactionStatus是PlatformTransactionManager执行事务过程中的状态接口它保存了当前事务的运行时信息。有人认为它只是数据载体但实际上它还提供了一些控制事务的方法具体接口定义如下public interface TransactionStatus extends SavepointManager, Flushable { boolean isNewTransaction(); // 当前是否开启了一个新事务 boolean hasSavepoint(); // 是否有保存点 void setRollbackOnly(); // 强制标记事务只能回滚 boolean isRollbackOnly(); // 事务是否已被标记为只能回滚 void flush(); // 刷新底层会话如Hibernate的Session boolean isCompleted(); // 事务是否已经完成 }setRollbackOnly这个方法要特别留意。当某个参与事务的组件发现数据出问题了但它本身无法决定整个事务的命运比如它是一个被内嵌调用的方法它可以调用setRollbackOnly给事务打上“回滚标记”。等到外层方法执行完事务管理器审视TransactionStatus时发现isRollbackOnly()返回true就会放弃提交、执行回滚。这个机制保证了参与同一事务的多个模块任何一个不满意最终整个事务都不会提交——当然这也会成为“明明没抛异常但事务莫名其妙回滚了”的经典坑位。TransactionStatus还继承了SavepointManager接口所以你可以在事务内部设置保存点Savepoint实现嵌套事务的部分回滚效果。这个机制比REQUIRES_NEW更轻量只是调用成本较低、通常不适用于普通业务场景但也算是理解Spring嵌套事务能力的一个窗口。4.4 三个接口之间的协作流程三个接口的关系用一个实际流程串联起来就非常清晰了。拿一个加了Transactional的Service方法举例当Spring AOP拦截到方法调用时会先拿到事务管理器调用getTransaction(definition)开启一个事务。这个方法内部会读取TransactionDefinition确定传播行为和隔离级别再从数据源里取一个数据库连接、设置好自动提交关闭以及隔离级别参数然后把相关的运行时状态封装成一个TransactionStatus对象返回。业务方法正常执行完抛不抛异常呢分两支不抛异常就调用commit(status)提交事务提交前判断TransactionStatus里的rollback-only标记抛了异常就根据异常类型决定是否调用rollback(status)。而这个rollback(status)执行时可能走完整回滚也可能基于保存点做局部回滚这些细节都由具体的实现类决定。如果把这个流程里的事务管理器换成你自己写的类你会发现业务代码完全不用改动。这正是接口抽象的价值——面向接口编程替换实现不影响现有逻辑。5. 事务管理器实现类DataSourceTransactionManager深入解析5.1 常见的事务管理器实现都有哪些在Spring生态里PlatformTransactionManager有多个实现类分别对接不同的持久化技术。我把最常见的几个列出来对比事务管理器实现类适用场景说明DataSourceTransactionManager使用JDBC / MyBatis / JdbcTemplate最常用直接管理DataSource连接的事务JpaTransactionManager使用Spring Data JPA / Hibernate兼顾JPA的EntityManager与事务同步JtaTransactionManager分布式事务由JTA规范实现用于跨多个数据源的分布式事务场景HibernateTransactionManager纯Hibernate SessionSpring Boot时代基本被JpaTransactionManager替代日常项目里你大概率只会遇到前两个。如果用的是MyBatis或者JdbcTemplateSpring Boot会根据DataSource自动配置DataSourceTransactionManager如果引入了Spring Data JPA则会自动配置JpaTransactionManager。5.2 DataSourceTransactionManager的工作机制DataSourceTransactionManager是绝大多数Web应用使用的默认事务管理器它的工作流程值得深挖。它的核心原理是从当前线程绑定的DataSource连接中开启一个数据库事务事务提交或回滚时操作的是同一个连接。Spring通过TransactionSynchronizationManager这个内部组件把数据库连接保存到ThreadLocal中确保同一线程内的多个DAO操作使用的是同一个Connection。所谓“事务的传播”本质上也依赖这个机制两个Service方法在同一个线程里调用它们拿到的是同一个数据库连接因此天然共享同一个数据库事务。一个值得注意的细节setAutoCommit(false)和setAutoCommit(true)的切换。事务管理器在获取连接后会先把自动提交关闭事务结束后再把自动提交恢复原状同时把连接归还给连接池。如果不经过Spring事务管理器的包装你手动从连接池拿连接去做事务控制稍不留神就会把连接的自动提交状态搞乱导致后续从连接池里拿到这个连接的其他请求出现奇奇怪怪的提交行为。5.3 DataSourceTransactionManager的核心源码逻辑直接看关键源码逻辑比翻文档更能理解事务管理器的运作。doBegin方法是事务开启的核心Override protected void doBegin(Object transaction, TransactionDefinition definition) { DataSourceTransactionObject txObject (DataSourceTransactionObject) transaction; Connection con null; try { if (!txObject.hasConnectionHolder()) { // 从数据源获取连接并绑定到当前线程 Connection newCon obtainDataSource().getConnection(); txObject.setConnectionHolder(new ConnectionHolder(newCon), true); } con txObject.getConnectionHolder().getConnection(); // 关闭自动提交由Spring接管事务边界 Integer previousIsolationLevel DataSourceUtils.prepareConnectionForTransaction(con, definition); txObject.setPreviousIsolationLevel(previousIsolationLevel); con.setAutoCommit(false); // 设置只读和超时 if (definition.isReadOnly()) { con.setReadOnly(true); } } catch (SQLException ex) { // 连接绑定失败时需要清理 DataSourceUtils.releaseConnection(con, obtainDataSource()); throw new CannotCreateTransactionException(...); } }doCleanupAfterCompletion方法负责事务结束后的善后工作恢复连接的自动提交状态、恢复原隔离级别、释放连接绑定关系最后把连接归还给连接池。protected void doCleanupAfterCompletion(Object transaction) { DataSourceTransactionObject txObject (DataSourceTransactionObject) transaction; Connection con txObject.getConnectionHolder().getConnection(); try { con.setAutoCommit(true); // 重置只读状态和隔离级别 con.setReadOnly(false); DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel()); } catch (SQLException ex) { // 记录日志 } txObject.getConnectionHolder().clear(); TransactionSynchronizationManager.unbindResource(obtainDataSource()); }我自己看源码的感受是Spring把最复杂的一层收在了事务管理器内部业务代码只需要配合声明式事务即可。但你要真正理解事务的开启、传播、回滚DataSourceTransactionManager这一层是绕不开的。5.4 如何在不同数据源下正确选型如果你在项目里配置了多个数据源或者用了读写分离方案事务管理器的选型会变得复杂一些。一个常见误区是给所有数据源配同一个事务管理器这会导致事务操作的连接归属出错。多数据源场景下的事务管理通常有几个策略每个数据源配一个独立的DataSourceTransactionManager事务上分离或者使用分布式事务管理器比如JTA实现来统一管理多个数据源的事务。前者简单可靠、性能可控适合数据源之间不用保证强一致的场景后者能保证跨数据源的数据一致性但引入的复杂度和性能损耗都很明显。项目中如果只是单纯的读写分离——一个主库一个从库——建议读操作不开启事务写操作走主库的事务管理器即可。给只读查询配置事务反而可能把连接绑定到主库破坏读写分离的预期效果。这个坑我见过不止一次值得记下来。6. 事务失效、回滚异常、连接泄漏这份排坑实录请收好6.1 回滚不生效的“头号元凶”受检异常我和很多人讨论过“为什么我的事务没回滚”这个问题排在第一位的答案永远是方法抛出了受检异常但Transactional默认根本不回滚受检异常。Transactional注解有个rollbackFor属性它决定了哪些异常触发回滚。Spring默认的策略是RuntimeException和Error触发回滚而IOException、SQLException这类受检异常不会触发回滚。你如果写了一个方法里面抛出Exception或者自定义受检异常不加配置的话事务会把数据提交掉。解决方案最直接的就是显式指定回滚异常类型Transactional(rollbackFor Exception.class) public void updateUser(User user) throws Exception { userDao.update(user); // 这里抛了受检异常事务依然会回滚 }经验之谈我在业务项目里倾向于统一使用rollbackFor Exception.class因为它最符合“有异常就回滚”的心智模型。你不需要在每种异常类型上纠结Spring到底回不回滚。当然如果你刻意希望某些异常不影响数据提交那就另说。6.2 事务不生效的“隐形陷阱”自调用与内部方法另一个高频问题是我在Service层写了一个方法A它没有加Transactional然后方法A内部调用了同一个类里加了Transactional的方法B。结果B的事务完全没有生效。原因是Spring的声明式事务基于AOP代理实现。外部调用进入的是Spring生成的代理对象代理对象会在目标方法前后开启和关闭事务。但方法A调用方法B时this指向的是原始对象而不是代理对象this.methodB()就相当于直接调用原始对象的方法AOP拦截器根本无从介入事务自然就没有开启。解决方案也常见三种把方法B挪到另一个Service类里通过注入的代理对象调用在类内部注入自身的代理对象通过Lazy注入避免循环依赖问题或者直接用AopContext.currentProxy()获取当前代理对象。三种方式各有适用场景最推荐的是第一种——把需要事务的方法拆到独立的Service类中既符合单一职责又顺手解决了自调用问题。这类问题的特点是编译不报错、运行不闪退日志里也没有报错信息但数据就是不对。排查时先用日志确认事务管理器是否被触发再检查调用链上有没有绕开代理对象。6.3 事务没提交就给别的方法“借走了”连接绑定与ThreadLocal有时候你会在代码里看到这样的逻辑在同一个事务里调用了异步线程去执行某些操作。表面看起来没什么毛病但异步线程里操作的数据时常出现不一致。原因在于Spring的事务连接是靠TransactionSynchronizationManager绑定到当前执行线程的ThreadLocal上的异步线程根本拿不到主线程的事务Connection。即便你使用了REQUIRED传播行为异步线程里没有绑定连接它只能新开一个连接新开一个数据库事务完全不跟主线程事务共享。如果此时异步线程里的操作失败了主事务回滚了异步线程的数据却已经提交了——事务一致性在这里彻底断裂。遇到事务与异步混用的场景我的做法是尽量把异步操作放在事务提交之后再执行利用TransactionalEventListener(phase TransactionPhase.AFTER_COMMIT)监听事务提交事件在事件处理器里再执行异步任务。这也是Spring生态里比较推荐的写法。6.4 长事务与连接池别让一个慢SQL拖垮全局长事务是一个容易被低估的问题。一个事务里执行了几十个操作或者事务内有慢SQL、远程调用、甚至休眠等待都会让事务长时间持有数据库连接不释放。如果并发请求一多连接池很快被打满后续请求全部阻塞在等待连接上整个服务“假死”。所以即便Spring允许你给事务设置任意超时时间我的建议仍然是事务方法里只做数据库操作别放远程HTTP调用、MQ发送这类外部IO。你可以在事务前后分别记录日志把耗时超过阈值的事务监控起来发现问题再针对性优化。6.5 实际问题速查表对照这些现象找原因现象大概率原因排查方向事务没回滚抛了受检异常没配rollbackFor检查异常类型与rollbackFor配置事务没开启同类内部调用了事务方法检查是否穿透代理、自调用数据不一致异步操作提交了异步线程不在同一事务中改用事务提交后事件触发异步偶发死锁并发更新同一批数据检查隔离级别与事务内SQL顺序方法加注解但完全没反应类未被Spring管理检查是否漏了Service等注解连接耗尽服务假死长事务不释放连接在事务内耗时、连接池监控上下功夫这六种问题基本覆盖了我日常排查的大多数场景。每一个后面都能再写一篇长文但对于“概述”这个定位来说先建立一个排查框架更重要细节问题留在后续章节慢慢展开。7. 我在日常开发中的一点体会如果你问我Spring事务管理这一章里最值得反复咀嚼的是什么我的答案不是某个接口的源码细节而是一个思维习惯先想清楚数据一致性边界在哪再动手写代码。实际项目里Transactional只是一个转瞬即逝的关键词但它背后是一整套复杂的运行时机制代理对象、传播行为、隔离级别、连接绑定、回滚策略……任何一个环节理解不到位线上都可能出现让你头皮发麻的数据异常。与其等出了问题再翻源码不如在写第一行事务代码之前就把这套机制的大致脉络装进脑子里。还有一个我个人的习惯无论是Transactional还是编程式事务我都会给所有对外暴露的事务方法写清晰的注释说明当前方法为什么需要事务、传播行为是什么、回滚预期是什么。这个习惯帮我节省了大量调试时间。毕竟事务本身的逻辑不难难的是团队里每个人都对事务有共识。下一篇可以接着聊事务传播行为的七种类型以及不同传播行为在嵌套调用、传播边界上的实战对比。先把这几个核心接口的理解沉淀下来后续内容会更扎实。