“你的项目里事务怎么失效的?”面试官靠在椅子上,眼神平静。这个问题听起来简单,但能回答到位的人,寥寥无几。Spring事务管理是Java开发的基石技能,而事务失效问题,堪称面试现场最常用的“照妖镜”。它能精准区分出一个人是背了八股文,还是真的有实战经验。今天,我们把Spring事务失效的各个场景,掰开了、揉碎了讲明白。
自调用:同一个类中的方法调用
这是我最开始踩过的坑。写了一个Service类,在里面定义了一个方法A,加了@Transactional,然后同一个类里的方法B调用了方法A。结果A方法抛了异常,数据竟然没回滚。
原因很直接:Spring的事务是通过AOP代理实现的。当一个类的方法被外部调用时,才会经过代理对象,代理对象负责开启事务、提交或回滚。但是,同一个类内部的this.methodA()调用,走的是原始对象,根本不是代理对象。代理压根没介入,事务自然失效。
怎么解决?简单粗暴的做法是,把需要事务的方法拆到另一个Service里,通过依赖注入调用。更高级一点,可以注入自己的代理:(YourService) AopContext.currentProxy(),然后用代理对象调用。不过最推荐的还是将事务边界清晰的方法独立出去,这样代码职责也单一。
异常处理偏差:@Transactional的默认回滚规则
写代码时心血来潮,在方法里捕获了异常,打印日志后吞掉,然后返回了一个正常结果。事务没回滚,数据写进去了。代码看起来没问题,可错误数据就这么产生了。
@Transactional注解的默认回滚策略是只对RuntimeException和Error进行回滚。如果你捕获了运行时异常,没让它抛出去,事务管理器根本不知道发生了异常,自然不会回滚。还有一种情况是抛出了Check Exception(受检异常),比如IOException、SQLException,事务默认也不会回滚。
解决方法有两个方向:一是别吞异常,让异常继续往外抛;二是如果必须捕获,在catch块里手动调用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。对于受检异常,可以在@Transactional里加上rollbackFor = Exception.class,告诉Spring所有异常都回滚。
方法修饰符限制:private、final、static方法的陷阱
这是个非常隐蔽的问题。为了提高代码封装性,把某个事务方法设成了private。运行起来一切正常,但是事务并没有生效。同理,加了final关键字的方法,也会导致事务失效。
Spring的默认代理方式是JDK动态代理或CGLIB,但无论是哪种,都无法代理private方法。因为private方法根本不会被包含在代理对象的逻辑中。final方法虽然能被CGLIB代理,但CGLIB是通过生成子类来代理的,final方法不能被重写,所以代理逻辑也无法切入。
static方法是类方法,不依赖于对象实例,事务管理器基于对象的代理模式完全无法控制static方法的生命周期。所以,事务方法必须用public修饰。这是硬性规定,没有商量余地。
数据库引擎不支持事务
这听起来像个笑话,但我真遇到过。部门新同事部署了一个新项目的表,所有DDL都是用MySQL的MyISAM引擎创建的。业务代码里Service层加了密密麻麻的@Transactional,结果数据在并发情况下疯狂不一致。排查了两天才发现,问题出在数据库层面。
MyISAM引擎压根不支持事务,Spring事务管理器再强大,也改变不了底层数据库的能力限制。事务管理器调用connection.commit()或connection.rollback(),这些SQL命令在MyISAM引擎上会被直接忽略,不会报错,但也不执行。
解决方案很简单,建表时使用InnoDB引擎。检查现有表可以用SHOW TABLE STATUS查看Engine字段。在项目初始化SQL脚本里,显式指定ENGINE=InnoDB。如果项目使用了JPA或Hibernate的自动建表,需要在配置里指定数据库方言并确认默认引擎。
传播行为配置错误
有一次写一个批量处理接口,外层方法调用了内层事务方法。业务要求内层方法独立提交,不影响外层。于是把内层方法配成了REQUIRES_NEW。但是测试发现,内层方法抛出异常时,外层事务依然被回滚了。
深挖之后才发现,传播行为配置错误只是表象,更深层的问题是同一个数据库连接被锁住了。当外层事务开启一个连接,内层方法REQUIRES_NEW会暂停外层事务,新建一个连接。但如果数据库连接池的defaultAutoCommit或相关事务隔离级别配置不当,内层事务可能在同一个连接上操作,导致事务依然耦合。
Spring事务传播行为有7种,最常用的是REQUIRED和REQUIRES_NEW。REQUIRED是默认的:如果当前有事务,就加入;没有则新建。REQUIRES_NEW是当前有事务时挂起,新建自己的事务。配置错误时,比如把内层方法配成SUPPORTS,如果外层有事务就加入,没有就算了。这会导致内层方法在无事务状态下执行,异常时不会回滚。
多线程调用:同一个事务中的异步陷阱
项目里有一个功能:处理一批订单,每个订单处理相对独立,但都涉及数据库写操作。为了提升性能,在Service方法上加@Transactional,然后内部开了线程池,并行处理每个订单。测试时发现,某个线程处理出错,数据只回滚了部分,其他线程写的数据还是提交了。
事务和线程是绑定的。Spring事务管理器使用ThreadLocal将数据库连接绑定到当前线程。新开的线程获取不到主线程的事务连接,自然开启的是全新事务。如果你在主线程里@Transactional,子线程的数据库操作根本不在主事务范围内。
正确做法是:如果必须确保多个线程的操作在同一个事务中,就不要使用多线程。并行处理和事务原子性是矛盾的。如果必须并行,要在子线程里手动管理事务,或者放弃事务原子性,使用补偿机制(如Saga模式)。
@Transactional注解加在非公有方法或接口上
虽然前面说过方法要用public,但还有一个更隐晦的问题:注解加在了接口方法上,而实现类没有加。如果Spring使用的是JDK动态代理,代理的是接口,那么接口上的注解会被读取。但如果代理方式变成了CGLIB(比如类没有实现接口,或者配置强制使用CGLIB),此时代理的是类,接口上的注解就被无视了。
最佳实践:永远把@Transactional加在实现类的方法上,而不是接口上。这样无论使用哪种代理方式,事务都能正常生效。而且从代码可读性角度,实现类才是真正执行数据库操作的地方,注解放在这里更符合直觉。
事务管理器配置不正确
Spring Boot确实做到了自动配置,但自动配置不代表万无一失。我之前遇到一个项目配置了多数据源,有两个DataSource,两个PlatformTransactionManager。在某个Service里,A数据源的事务管理器配成了primary,另一个B数据源的操作加上了@Transactional,结果B数据源的写操作无法回滚。
每个数据源都需要对应一个PlatformTransactionManager。Spring Boot虽然有默认的事务管理器,但在多数据源场景下,必须显式指定使用哪个。默认情况下,Spirng会使用名为transactionManager的Bean。如果你的另一个事务管理器叫secondTransactionManager,必须在@Transactional注解里指定:@Transactional("secondTransactionManager")。
还有更隐蔽的情况:事务管理器配置了错误的DataSource。比如事务管理器A绑定了数据源A,但在事务方法里访问了数据源B的表。此时数据源B的操作在事务管理器A的管理范围之外,不会参与事务。
AOP顺序错误
这个场景比较高级。当你的项目里同时使用了@Transactional和自定义AOP切面(比如日志切面、权限切面)时,AOP的执行顺序可能导致事务失效。
如果自定义切面的执行顺序比事务切面更靠外(优先级更高),在切面里捕获了异常并且没有重新抛出,事务根本感知不到异常。因为事务切面在更内层,异常已经被外层切面吞掉了。
解决方案:通过@Order注解或实现Ordered接口控制切面顺序。事务切面应该在最外层,也就是Order值最小(优先级最高)。Spring默认的事务切面Order值是Integer.MAX_VALUE(最低优先级),所以自定义切面需要设置为比这个更小的值,或者在自定义切面里确保异常继续抛出。
写在最后:面试官真正想听什么
回到最初的面试场景。当面试官问“Spring事务失效的几种场景”,他期待的不仅仅是背诵清单。他真正想听的是:你踩过哪些坑,是怎么定位的,最终如何解决的。
我建议你在回答时,先说出四五种核心场景:自调用、异常处理不当、方法修饰符问题、传播行为错误、多数据源事务管理器配置。然后挑一个你印象最深的场景,详细描述当时的问题表现、排查过程、解决方案。比如“有一次我用@Transactional配合线程池,结果数据只回滚了一部分……”。
知道知识是第一步,能把失败经验讲成故事,才是面试的致命武器。别为了显得无所不知而把所有场景全罗列出来,那样会显得像在背文档。挑重点,讲细节,加实例,面试官会对你刮目相看。
Spring事务失效,说到底,是因为它依赖于代理机制。代理不生效,事务就无从谈起。理解代理机制,理解AOP的执行过程,你就掌握了判断事务是否生效的终极方法。以上这些场景,你在实际工作中遇到过几个?不妨从今天开始,在自己的项目里留意一下,下个面试官的问题,你就能从容应对。