☰
Spring事务核心原理与实战避坑:从@Transactional到事务失效排查
2026/10/21 9:04:32 网站建设 项目流程

事务这块儿,凡是做后端的人迟早都要啃。很多人背过八股,知道@Transactional加在方法上就能回滚,知道隔离级别有四种,但一问到“Spring事务到底是怎么实现的?为什么有时候注解加了却不好使?”就卡住了。这篇文章我把Spring事务的实现方式、底层原理、隔离级别、传播行为还有那些经典的坑一次性讲透,用实际开发中的例子串起来,希望能帮你把这块知识彻底焊死在脑子里。

1. 事务实现方式:从编程式到声明式的演进

1.1 编程式事务:最原始但最灵活的方式

Spring事务的实现方式归根结底就两大类:编程式事务和声明式事务。编程式事务是底子,声明式事务是在它之上做的封装,理解了底子才能明白封装到底解决了什么问题。

编程式事务说白了就是自己写代码控制事务的开启、提交和回滚。早期开发中,常见的写法是直接注入PlatformTransactionManager,然后手动调用它的方法:

@Service public class OrderService { @Autowired private PlatformTransactionManager transactionManager; public void createOrder(Order order) { DefaultTransactionDefinition def = new DefaultTransactionDefinition(); def.setIsolationLevel(TransactionDefinition.ISOLATION_DEFAULT); def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); TransactionStatus status = transactionManager.getTransaction(def); try { // 业务代码:插入订单、扣减库存等 orderDao.insert(order); stockDao.decrease(order.getProductId(), order.getCount()); // 手动提交 transactionManager.commit(status); } catch (Exception e) { // 手动回滚 transactionManager.rollback(status); throw e; } } }

这种方式的优点是控制粒度非常细,事务边界完全由自己掌控,适合那些有复杂事务划分逻辑的业务场景。缺点也显而易见:模板化代码大量重复,每个需要事务的方法都要写一套getTransaction、try-catch、commit/rollback,侵入性太强,业务代码里塞满了事务控制的逻辑,维护起来极其痛苦。

后来Spring提供了TransactionTemplate,算是对原始API的一种简化:

@Service public class OrderService { @Autowired private TransactionTemplate transactionTemplate; public void createOrder(Order order) { transactionTemplate.execute(status -> { orderDao.insert(order); stockDao.decrease(order.getProductId(), order.getCount()); return null; }); } }

TransactionTemplate把getTransaction、commit、rollback这些重复劳动收进了模板方法里,代码清爽了不少。它的底层依然是PlatformTransactionManager,在execute方法内部完成了事务的开启、执行、提交和异常回滚。

但即便如此,编程式事务依然没有解决“事务逻辑和业务逻辑分开”的问题——你还是在业务方法里显式地写了事务相关的调用。对于绝大多数业务系统来说,最理想的情况是:我声明一下这个方法需要事务,事务的开关、提交、回滚都由框架帮我搞定。

1.2 声明式事务:@Transactional注解背后的机制

于是@Transactional注解出现了,这是Spring声明式事务的核心体现。开发者只需要在方法或类上标一个注解,Spring的事务管理就会自动对这个方法进行事务增强。

为什么加个注解就能有事务?答案就是AOP(面向切面编程)。Spring在启动时扫描到被@Transactional标记的方法或类,会为它们生成代理对象,在代理对象中织入事务管理的逻辑。你调用业务方法时,实际走的是代理对象的方法,代理会在方法执行前开启事务,方法正常返回时提交事务,方法抛出异常时回滚事务。

这是一个非常重要的事实:Spring事务生效的前提是,你调用的方法必须通过Spring的代理对象来调用。这一点后面讲事务失效时会反复提到。

声明式事务的好处是事务逻辑被完全剥离,业务代码里看不到一行事务控制的代码。它适合绝大多数常规业务场景:一个方法对应一个事务边界,异常即回滚。

但需要注意的是,声明式事务没那么“灵活”,它是基于方法级别的拦截,事务颗粒度是方法本身。如果你真的需要在同一个方法中间切换事务或者分阶段提交,声明式事务就力不从心了,这种情况还是得回到编程式事务。

1.3 XML配置与注解配置的取舍

在Spring早期版本(以及Spring Boot普及之前),声明式事务除了注解,还有一套XML配置的方式。我当时刚入行的时候还见过这种配置写法:

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="insert*" propagation="REQUIRED"/> <tx:method name="update*" propagation="REQUIRED"/> <tx:method name="delete*" propagation="REQUIRED"/> <tx:method name="get*" read-only="true"/> </tx:attributes> </tx:advice> <aop:config> <aop:pointcut id="servicePointcut" expression="execution(* com.example.service.*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="servicePointcut"/> </aop:config>

这种方式通过XML定义事务的传播行为、隔离级别、回滚规则,通过AOP切点指定哪些方法被事务拦截。在Spring Boot和Java Config全面普及之后,XML配置方式已经基本退出了历史舞台。

从实现方式的角度做个总结:

实现方式侵入性灵活性适用场景
编程式事务(PlatformTransactionManager)高最高事务边界复杂、需精细控制
编程式事务(TransactionTemplate)中较高中等复杂度事务逻辑
声明式事务(@Transactional)低中大多数业务场景,方法级事务
声明式事务(XML配置)低中传统Spring配置项目

2. 事务原理拆解:代理、拦截器与事务管理器

2.1 动态代理:Spring事务的基石

前面提到,@Transactional之所以能生效,核心在于代理。Spring AOP的代理机制有两种:JDK动态代理和CGLIB代理。

JDK动态代理要求被代理的类必须实现接口,它通过java.lang.reflect.Proxy在运行时生成一个代理类,代理类和目标类实现相同的接口,通过InvocationHandler拦截方法调用。如果目标类没有实现任何接口,Spring就退回到CGLIB代理——CGLIB通过继承目标类生成子类,在子类中覆写目标方法,实现方法拦截。

这里有个细节:Spring Boot 2.x之后,spring.aop.proxy-target-class默认为true,也就是说即使目标类实现了接口,默认也是用CGLIB代理。这也是为什么你在调试时看到的代理对象是com.example.service.OrderService$$EnhancerBySpringCGLIB$$xxx这样的类名。

理解代理机制的意义在于:事务拦截的逻辑就在这个代理对象的拦截器里,你调用目标方法时,拦截器会在目标方法前后执行事务的逻辑。如果你拿到的不是代理对象,而是直接new出来的目标类对象,事务注解就是一张废纸。

2.2 事务拦截器的工作流程

当代理对象的方法被调用时,真正的处理逻辑在TransactionInterceptor这个AOP拦截器中。它内部的工作流程可以拆成五步:

第一步,解析方法上的事务属性。TransactionInterceptor拿到当前方法后,会去解析@Transactional注解上的配置项:传播行为、隔离级别、超时时间、只读标志、回滚规则等。如果类上有注解而方法上没有,方法上的注解会覆盖类上的配置。

第二步,根据事务属性获取TransactionManager。这里有个选择逻辑:如果注解上指定了transactionManager属性(或value属性),就使用指定的管理器;如果没有指定,默认使用容器中的PlatformTransactionManager。

第三步,开启事务。调用transactionManager.getTransaction()获取事务状态。这一步骤实质上是让底层资源管理器开始事务——对JDBC事务来说,就是获取数据库连接,设置autoCommit为false,然后执行connection.commit()和connection.rollback()的掌控权被Spring接管。

第四步,执行业务方法。通过InvocationCallback调用真正被代理的目标方法,也就是你的业务代码。

第五步,根据执行结果决定提交还是回滚。方法正常返回,spring调用commit;方法抛出异常,spring调用rollback。这是最需要注意的地方——异常触发回滚是有条件的,后面细讲。

整个过程可以用一段伪代码来描述:

// TransactionInterceptor的简化逻辑 public Object invoke(MethodInvocation invocation) throws Throwable { TransactionInfo txInfo = completeTransactionInfo(invocation); Object retVal; try { retVal = invocation.proceed(); // 执行目标方法 } catch (Throwable ex) { // 异常时回滚,但要看异常类型是否匹配回滚规则 completeTransactionAfterThrowing(txInfo, ex); throw ex; } // 正常返回时提交 commitTransactionAfterReturning(txInfo); return retVal; }

2.3 事务同步管理器与Connection的绑定

TransactionSynchronizationManager是理解Spring事务原理时绕不开的一个类,它内部维护了一个ThreadLocal:

private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");

这个ThreadLocal里存的关键信息包括:当前线程绑定的数据库连接(DataSource和Connection的映射关系)、当前事务是否活跃、事务同步回调列表等。

为什么要这样做?因为一个业务方法里可能调用多个DAO方法,每个DAO方法都需要从数据源获取连接。如果每个DAO方法各自从连接池拿一个连接,那它们操作的就是不同的数据库会话,事务就不可能统一控制。Spring的解决方案是:事务开启时从DataSource获取一个连接,把这个连接绑定到当前的ThreadLocal上。后续所有DAO方法通过DataSourceUtils.getConnection()获取连接时,都会优先从ThreadLocal里拿已经绑定的那个连接,而不是重新从连接池获取。

这个过程就是事务能够跨多个DAO方法保持一致性的基础。理解了这个机制,你也就理解了为什么Spring事务默认不支持多线程——ThreadLocal的数据只对当前线程可见,子线程拿不到父线程的事务上下文。这也是为什么事务方法里用new Thread或者@Async异步调用,事务管理会乱套。

3. 隔离级别详解:从理论到数据库落地

3.1 四种隔离级别与并发问题对照

事务隔离级别解决的是并发事务之间的可见性问题。SQL标准定义了四种隔离级别,分别对应三种并发问题的不同防御程度。

先说并发问题。多个事务同时操作数据时可能产生的问题主要有三种:

  • 脏读:事务A读取了事务B未提交的数据,之后B回滚了,A读到的就是脏数据。
  • 不可重复读:事务A内两次读取同一行数据,期间事务B修改了这行数据并提交,A两次读到的结果不一致。
  • 幻读:事务A内两次查询同一范围的数据,期间事务B往这个范围插入或删除了数据并提交,A两次查到的记录条数不一致。

四种隔离级别对这三个问题的防御能力逐步增强:

隔离级别脏读不可重复读幻读
READ_UNCOMMITTED(读未提交)可能可能可能
READ_COMMITTED(读已提交)不可能可能可能
REPEATABLE_READ(可重复读)不可能不可能可能
SERIALIZABLE(串行化)不可能不可能不可能

隔离级别越高,并发能力越差,性能损耗越大。所以实际开发中要在数据一致性和并发性能之间做取舍。

3.2 MySQL和Oracle在隔离级别上的差异

这里必须强调一个大多数人踩过的坑:Spring定义的隔离级别只是“期望值”,最终生效的是什么隔离级别,取决于数据库的实际支持和默认配置。

MySQL的InnoDB引擎默认隔离级别是REPEATABLE_READ。而且MySQL在可重复读隔离级别下,通过next-key lock(间隙锁+行锁)机制,对幻读也做了很大程度的规避。也就是说,MySQL的可重复读实际防住了幻读的大部分场景,并不是标准文档里说的“可能幻读”。

Oracle数据库的默认隔离级别是READ_COMMITTED。Oracle也支持SERIALIZABLE,但不支持READ_UNCOMMITTED。如果你在Oracle上设置了ISOLATION_READ_UNCOMMITTED,Oracle并不会真正按这个级别工作,而是会给你一个高于这个级别的保证。

还有SQL Server,它的默认隔离级别是READ_COMMITTED,但它的实现是锁机制而非MVCC,在高并发下表现和MySQL差异很大。

所以在配置隔离级别时,不要只看Spring文档,还要看清楚你用的数据库引擎支持哪些级别。Spring的@Transactional(isolation = Isolation.REPEATABLE_READ)只是请求事务管理器按这个级别开启事务,如果底层数据库不支持,要么被降级,要么启动时报错。

3.3 隔离级别的配置方式与常见坑

Spring中通过@Transactional的isolation属性设置隔离级别:

@Transactional(isolation = Isolation.REPEATABLE_READ) public void processOrder() { // 业务逻辑 }

Isolation枚举的取值对应关系:

枚举值常量值对应JDBC值
DEFAULT-1使用数据库默认级别
READ_UNCOMMITTED1Connection.TRANSACTION_READ_UNCOMMITTED
READ_COMMITTED2Connection.TRANSACTION_READ_COMMITTED
REPEATABLE_READ4Connection.TRANSACTION_REPEATABLE_READ
SERIALIZABLE8Connection.TRANSACTION_SERIALIZABLE

一个非常容易踩的坑是:设置了isolation但连接的MySQL数据库默认就是可重复读,你以为自己升级到了可重复读,其实一直是默认级别。另外一个坑是,很多人在只读查询上也设置了很高的隔离级别,比如SERIALIZABLE,这在并发量大的系统里是灾难性的,锁竞争会把数据库拖垮。

实际经验:对于并发比较高的交易系统,优先考虑READ_COMMITTED,因为它在一致性要求和并发性能之间最平衡。MySQL默认的REPEATABLE_READ也没问题,但不建议为了“更安全”盲目升到SERIALIZABLE——这隔离级别的代价是极端性能损耗和潜在的死锁风险。

4. 传播行为:多事务方法嵌套时的行为编排

4.1 七种传播行为速查

事务的传播行为解决的是:当一个事务方法调用另一个事务方法时,这两个方法的事务如何组织在一起。Spring定义了七种传播行为:

传播行为含义典型场景
REQUIRED如果当前有事务则加入,没有则新建最常用,默认值
REQUIRES_NEW无论当前有没有事务,都挂起当前事务,新建一个独立事务日志记录、独立操作的异步事务
NESTED如果当前有事务,创建嵌套事务(Savepoint);没有则新建批量处理中的局部回滚
SUPPORTS如果有事务则加入,没有则在非事务状态下执行查询方法
NOT_SUPPORTED挂起当前事务,以非事务方式执行事务中执行不需要事务的次要操作
MANDATORY必须有事务,否则抛异常强制要求事务上下文的操作
NEVER必须没有事务,存在事务则抛异常事务中执行不应被事务包裹的资源操作

默认的REQUIRED是绝大多数业务方法的传播行为:外层方法开了一个事务,内层方法不会另起炉灶,而是直接加入外层事务。这也意味着内外层方法共享同一个事务边界,内层方法的异常会导致整个外层事务回滚,反过来,内层方法的提交并不会真正提交,要等最外层方法提交后事务才算完成。

4.2 从开发场景选择传播行为

选择传播行为不是背定义,而是要放在具体的业务场景里判断。

最常见的场景是:主业务方法里调用了一个子方法,子方法抛了异常,你希望整个事务都回滚——这个用默认的REQUIRED就够了。因为子方法加入主事务,子方法异常往外抛,主方法接收到异常后事务回滚。

反过来,有一种场景是:主业务逻辑出错了,但你依然希望某些操作被持久化,比如记录异常日志、发送审计信息。这时候如果子方法用的是REQUIRED,主事务回滚时,日志记录的写入也一起回滚了,日志就没了。这种场景要给子方法用REQUIRES_NEW,让它在独立事务中执行,不受主事务回滚的影响。

还有一种比较复杂的场景,就是批量任务中希望部分成功、部分失败时不互相拖累。比如每天定时批量处理一批订单,处理到第100个订单时出错了,你不希望前99个已经处理成功的订单也跟着回滚(否则下次任务又要从头跑)。这种场景可以用REQUIRED加捕获异常的方式逐条处理,或者用NESTED去做局部回滚。

伪代码:

@Transactional public void processBatch(List<Order> orders) { for (Order order : orders) { try { processOne(order); // 这个方法是REQUIRED,加入外部事务 } catch (Exception e) { // 记录失败订单,继续处理后面的 } } }

但这种写法有个陷阱:processOne内部如果操作了数据库并抛异常,因为它是REQUIRED,加入的是外部事务,它的任何数据库变更都还没提交,捕获异常后继续循环,外部的整体事务最终还是会提交前99个订单的变更——这个行为恰恰是你想要的。但如果你在processOne内部做了某些操作触发了局部脏数据(比如数据状态被修改了但没继续处理),捕获异常后这部分修改仍然在未提交状态中,后续事务提交时会被一并提交,这就是经典的“部分成功部分失败却糊里糊涂失败”问题。对于这种逐条隔离的需求,更严格的做法是用REQUIRES_NEW或NESTED。

4.3 REQUIRES_NEW与NESTED的底层差异

这两个传播行为在外部表现上有些相似,但底层机制完全不同,很多面试题也喜欢在这上面做文章。

REQUIRES_NEW是真正的新事务。它的实现是:挂起当前事务(暂停当前事务的资源绑定),然后从数据源重新获取一个数据库连接,在全新的连接上开启新事务。新事务提交时,只提交新连接上的操作;之后恢复旧事务,继续用原来的连接执行后续操作。

NESTED则不同,它并不创建新的事务,而是在当前事务中创建一个保存点(Savepoint),本质上还是同一个事务、同一个数据库连接。嵌套事务的回滚是回到保存点,并不是真正的回滚整个事务。外部事务如果最终回滚,嵌套事务做的修改也会一起回滚;外部事务最终提交,嵌套事务也会一并提交。

用一张表来区分:

维度REQUIRES_NEWNESTED
数据库连接新获取一个连接复用当前事务的连接
是否独立提交是否
内部回滚影响只回滚内部操作回滚到保存点
外部回滚时内部是否回滚外部回滚不影响已提交的内部事务内部操作一并回滚
数据库依赖依赖事务管理器依赖数据库的Savepoint支持

从实用角度讲,NESTED本质上解决的是“部分回滚”的需求,而且不破坏外层事务的整体性。但要注意,MySQL的InnoDB支持Savepoint,所以NESTED在MySQL上可以使用。如果换成某些不支持Savepoint的数据库,NESTED就可能退化为和REQUIRED一样的行为。

5. 事务失效排查与避坑指南

5.1 事务不生效的典型场景

搞明白原理后,再来看那些“@Transactional加了跟没加一样”的问题,就清晰多了。事务失效的场景有几种典型情况,我在实际项目中基本都遇到过。

第一种,方法不是public。Spring的声明式事务默认是通过代理拦截public方法实现的。如果你把@Transactional标在private或protected方法上,代理无法拦截,事务自然不生效。Spring的官方文档明确说过,protected和private方法上的@Transactional不会抛错,但也不会有任何事务效果。所以在实际开发中,事务注解必须放在public方法上。

第二种,类没有被Spring管理。如果@Transactional所在的类没有被Spring容器扫描到(没有@Component、@Service等注解),Spring根本不知道这个类的存在,更不可能为它创建代理对象,事务注解完全无效。

第三种,异常被捕获了但没抛出。请看这个经典代码:

@Transactional public void createOrder() { try { orderDao.insert(order); stockDao.decrease(productId, count); } catch (Exception e) { log.error("下单失败", e); // 这里没有把异常抛出,事务不会回滚 } }

异常被捕获且没有抛出去,拦截器就认为方法成功执行了,于是提交事务。底层数据已经写到一半(有的成功有的失败),但是事务提交了,这样数据就乱了。解决方式是:要么不要捕获异常,让异常抛出去触发回滚;要么在catch块中显式地抛出一个RuntimeException:

@Transactional public void createOrder() { try { orderDao.insert(order); stockDao.decrease(productId, count); } catch (Exception e) { log.error("下单失败", e); throw new RuntimeException("下单失败", e); } }

第四种,异常类型不在回滚范围内。Spring事务默认只在抛出RuntimeException或Error时回滚,受检异常(checked exception,如IOException、SQLException)默认不回滚。这个设计理念是:受检异常代表“业务上可处理的异常”,比如余额不足,框架认为这个方法执行本身的“事务意图”是完成的;而运行时异常代表不可恢复的错误。如果你希望在某个受检异常发生时也回滚,需要在注解上显式声明:

@Transactional(rollbackFor = Exception.class)

很多老代码里会写@Transactional,然后方法里抛出一个自定义的受检异常,结果数据没回滚,查了半天才找到原因。一个比较省心的做法是在团队规范里统一要求:@Transactional的rollbackFor一律加上Exception.class,除非有特殊理由不加。

5.2 自调用问题详解

自调用问题非常经典,也是面试官最爱问的“为什么事务失效”场景之一。

@Service public class OrderService { public void processOrder() { // 自调用,this.createOrder()不会经过Spring代理 createOrder(); } @Transactional public void createOrder() { // 事务逻辑 } }

外部调用processOrder()时,这个调用会通过OrderService的代理对象进入。但在方法内部this.createOrder()是直接调用了当前对象的原始方法,没有再次经过代理对象,因此@Transactional注解不会被拦截器解析,事务不生效。

这个问题的本质就是:Spring的声明式事务是代理实现的,只有“外部调用代理对象的方法”才会触发事务拦截。this指针指向的是原始对象而不是代理对象,自然没有拦截逻辑。

解决方案有几种:

第一种,注入自身代理对象:

@Service public class OrderService { @Autowired @Lazy private OrderService self; public void processOrder() { self.createOrder(); // 通过代理对象调用 } @Transactional public void createOrder() { // ... } }

第二种,把需要事务的方法挪到另一个独立的Service中,由外部Service调用:

@Service public class OrderService { @Autowired private StockService stockService; public void processOrder() { stockService.createOrder(); // 调用另一个代理对象 } }

第三种,也可以把openSessionInView体系中的TransactionTemplate当作方案,直接用编程式事务包裹createOrder的内部逻辑,就不依赖代理了。

从项目规范角度来说,我的建议是:尽量让@Transactional标注的方法在类外被直接调用,而不是设计成同类的内部互相调用。如果你的系统里确实有这种内部调用需求,务必使用注入自身代理或拆分Service的做法。

5.3 排查方法与调试工具实录

实际排查一个事务不生效的问题,我一般按下面这个顺序走:

第一步,确认类被Spring管理。看启动日志里有没有扫描到这个类,或者加个@Resource注入后看是不是代理对象(打印getClass()就能看出来)。

第二步,确认方法修饰符是public。这个用肉眼扫一遍就行。

第三步,看异常有没有被吞掉。在方法里打断点,看异常是否被catch后没有改抛。

第四步,看异常类型。如果抛出的是受检异常,右键看异常继承树,确认是不是RuntimeException的子类。

第五步,确认数据库引擎支持事务。MySQL的MyISAM引擎就不支持事务,InnoDB才支持。如果表是MyISAM,你配置再多的Spring事务也没用。

第六步,确认是不是在事务方法里用了多线程。子线程里执行的目标方法会脱离主线程的事务上下文,因为TransactionSynchronizationManager用的是ThreadLocal。

还有一个辅助排查的实用技巧:开启Spring事务的日志,在application.properties里配置:

logging.level.org.springframework.transaction.interceptor=TRACE

这样执行的过程中会打印事务的开启、提交、回滚日志,看到Getting transaction for [...]和Completing transaction for [...]这些关键字,你就知道事务确实被代理拦截到了。如果只有Completing transaction但日志显示commit而不是rollback,说明你的回滚条件没满足。

日志长这样:

TRACE o.s.t.i.TransactionInterceptor - Getting transaction for [com.example.service.OrderService.createOrder] TRACE o.s.t.i.TransactionInterceptor - Completing transaction for [com.example.service.OrderService.createOrder] after exception: xxx

看到after exception后面跟着异常信息,并且最终是rollback而不是commit,说明事务回滚成功。如果异常信息后面跟的还是commit,那就是异常类型不在回滚规则内。

5.4 事务同步、连接池与多线程的坑

除了上面这些经典的“事务失效”问题,实际项目里还有几个隐蔽的坑值得单独拿出来说。

超时时间不生效的坑。@Transactional(timeout = 3)这个配置表示事务在空闲超过3秒后自动回滚。但它的计算方式是事务开始到当前的“空闲时间”?不对——超过3秒后,DataSourceTransactionManager会在下一次数据库操作时抛出超时异常。比如一个事务做了第一次SQL后停留5秒(比如远程调了外部接口且耗时5秒),然后执行第二个SQL——这个时候超时异常会抛出,事务会回滚。但如果整个事务的数据库操作在3秒内全部完成,即使中间有一些非数据库操作的耗时逻辑,只要没有新的数据库操作触发超时检查,这个超时配置其实起不到“严格限制整个事务耗时”的作用。指望用它控制整个方法的执行时长是不准确的。

只读事务的优化。@Transactional(readOnly = true)在JDBC层面实际只是设置了连接为只读,connection.setReadOnly(true)。它对MySQL来说只是一个“提示”,并不会真正禁止写操作——你执行insert依然能成功(某些数据库,如PostgreSQL,会真正拒绝写操作)。在MySQL上它主要的好处是可以把事务设置为只读快照,以及允许部分数据库驱动做性能优化。

大事务问题。如果一个事务方法里执行了大量数据操作,或者放了一些耗时很长的外部调用,这个事务会长期持有数据库连接,额外增加了连接池的压力。更严重的是,长事务会让数据库的undo log积累大量数据,影响数据库的性能。解决思路就是把大事务拆成小事务,把非核心的耗时操作挪到事务外面。多数情况下,这是比追求高级隔离级别更有效的性能优化手段。

多数据源事务的复杂性。如果系统里配置了多个数据源,@Transactional默认只会使用指定的一个数据源。比如你配置了两个DataSource,Spring会为每个数据源生成一个独立的DataSourceTransactionManager,通过@Transactional("txManagerA")来指定使用哪个事务管理器。如果需要跨数据源保持一致事务,那就得引入分布式事务方案了,比如可靠消息、本地消息表、Seata等。这是一个系统的取舍和架构问题,不是加个注解就能搞定的。

6. 写在最后的经验总结

我在实际项目里踩过最多的坑,排序下来大概是这样的:第一是异常被吞导致事务不生效,第二是自调用问题,第三是受检异常不回滚。这三类问题占了事务问题排查的八成以上。

个人建议是,在项目里做一个统一的事务规范:

  • @Transactional注解必须明确指定rollbackFor,统一用Exception.class
  • 事务注解只标在public方法上
  • 事务方法内不捕获异常,要么让异常向上抛,要么捕获后改抛运行时异常
  • 事务方法内不做远程调用、不做耗时操作
  • 一个事务方法的管理范围尽量精简,避免大事务
  • 对事务不生效的疑问,第一时间打日志看事务拦截器是否介入

事务这块知识,把代理机制、事务管理器、隔离级别、传播行为串起来之后,就像一张地图,你知道每条路往哪走、哪里有陷阱,出问题的时候就不慌了。希望这篇总结对你也有同样的效果。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询