Spring事务的7种失效场景:每种都有完整代码复现和修复方案
Spring事务的7种失效场景:每种都有完整代码复现和修复方案
适读人群:Java后端开发 | 难度:★★★★☆ | 出现频率:极高
开篇故事
两年前我接手了一个支付系统的维护工作,上线后发现了一个非常诡异的bug:转账操作,扣款成功了,加款却没有成功,但数据库里没有任何异常记录,事务日志也没有回滚。
查了两天,终于找到问题:加款方法被抽出来作为一个public方法,在同一个类里被转账方法直接调用。加款方法上有@Transactional注解,但因为是同类内部调用,Spring的代理拦截不到,事务根本没开启。
扣款有事务(被外部调用),加款没有事务(内部调用)。出错时加款操作直接数据不一致,没有报错,没有回滚,悄无声息地出了故障。
这是Spring事务失效里最典型的一种。今天我把7种失效场景全部用代码演示给你看,每种都有修复方案。
一、高频考点拆解
Spring事务失效是面试必考题,考察点有三个层次:
第一层:能说出哪些场景会导致事务失效(结论) 第二层:能说清楚每种场景失效的原因(原理) 第三层:能给出修复方案,并说明修复的原理(工程能力)
第三层是大厂真正要考的,不只是背答案,而是真的在项目里踩过坑、解决过问题。
二、深度原理分析
Spring事务的底层机制
Spring事务基于AOP(面向切面编程),底层用动态代理实现。
Spring有两种代理方式:
- JDK动态代理:基于接口,被代理类必须实现接口
- CGLIB代理:基于子类,可以代理没有接口的类
所有的事务失效,本质上都是代理没有生效(绕过了代理直接调用目标方法),或者事务配置不符合回滚条件。
三、7种失效场景 + 代码验证
失效场景1:同类内部调用(最常见!)
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
// 外部调用这个方法,事务正常
public void createOrder(Order order) {
// 问题!同类内部直接调用addDetail,
// this.addDetail()调用的是原始对象,不是代理对象
// addDetail的@Transactional完全失效
addDetail(order);
orderMapper.insertOrder(order);
}
@Transactional
public void addDetail(Order order) {
// 这里的事务注解不生效!
orderMapper.insertDetail(order);
}
}原因:Spring事务通过代理实现,外部调用走代理,代理拦截到@Transactional注解后开启事务。但同类内部调用this.addDetail(),this指向的是原始对象,不经过代理,所以事务注解不起作用。
修复方案1:把被调用方法移到另一个Service中
@Service
public class OrderService {
@Autowired
private OrderDetailService orderDetailService; // 注入另一个Service
public void createOrder(Order order) {
orderDetailService.addDetail(order); // 走代理,事务生效
orderMapper.insertOrder(order);
}
}@Service
public class OrderDetailService {
@Transactional
public void addDetail(Order order) {
orderMapper.insertDetail(order); // 事务正常生效
}
}修复方案2:通过AopContext获取当前代理对象(需要开启exposeProxy=true)
@Service
public class OrderService {
public void createOrder(Order order) {
// 通过代理对象调用,事务生效
((OrderService) AopContext.currentProxy()).addDetail(order);
orderMapper.insertOrder(order);
}
@Transactional
public void addDetail(Order order) {
orderMapper.insertDetail(order);
}
}
// 还需要在启动类或配置类添加:
@EnableAspectJAutoProxy(exposeProxy = true)失效场景2:方法不是public的
@Service
public class PaymentService {
// 事务失效!@Transactional在非public方法上不生效
@Transactional
private void doPayment(Long orderId) {
// 扣款操作
}
// 事务失效!protected也不行
@Transactional
protected void chargeUser(Long userId) {
// 扣费操作
}
}原因:Spring AOP(无论JDK代理还是CGLIB代理)都只能拦截public方法。Spring文档明确说明@Transactional只能用于public方法。
修复方案:将方法改为public。
@Service
public class PaymentService {
@Transactional // 现在生效
public void doPayment(Long orderId) {
// 扣款操作
}
}失效场景3:异常被吞掉
@Service
public class UserService {
@Transactional
public void updateUser(User user) {
try {
userMapper.update(user);
sendNotification(user); // 假设这里抛出异常
} catch (Exception e) {
// 事务失效!异常被捕获了,Spring感知不到异常,不会回滚
log.error("更新用户失败", e);
}
}
}原因:Spring事务的回滚是在方法抛出异常后由代理触发的。如果异常被捕获且没有再抛出,Spring代理感知不到异常发生,不会触发回滚。
修复方案1:捕获后重新抛出
@Transactional
public void updateUser(User user) {
try {
userMapper.update(user);
sendNotification(user);
} catch (Exception e) {
log.error("更新用户失败", e);
throw e; // 重新抛出,让Spring感知到异常
}
}修复方案2:手动标记回滚
@Transactional
public void updateUser(User user) {
try {
userMapper.update(user);
sendNotification(user);
} catch (Exception e) {
log.error("更新用户失败", e);
// 手动标记当前事务需要回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}失效场景4:异常类型不匹配
@Service
public class InventoryService {
// 事务失效!默认只回滚RuntimeException和Error
// IOException是受检异常,默认不触发回滚
@Transactional
public void deductStock(Long productId, int qty) throws IOException {
inventoryMapper.deduct(productId, qty);
if (qty > getCurrentStock(productId)) {
throw new IOException("库存不足"); // 抛出受检异常,不会回滚!
}
}
}原因:Spring事务默认只在RuntimeException和Error时回滚,受检异常(checked exception,继承Exception但不继承RuntimeException的)不触发回滚。
修复方案:指定回滚的异常类型
// 方案1:指定所有Exception都回滚
@Transactional(rollbackFor = Exception.class)
public void deductStock(Long productId, int qty) throws IOException {
inventoryMapper.deduct(productId, qty);
if (qty > getCurrentStock(productId)) {
throw new IOException("库存不足"); // 现在会回滚
}
}
// 方案2:改用RuntimeException
@Transactional
public void deductStock(Long productId, int qty) {
inventoryMapper.deduct(productId, qty);
if (qty > getCurrentStock(productId)) {
throw new RuntimeException("库存不足"); // 会回滚
}
}失效场景5:未被Spring管理的类
// 事务失效!没有@Service或@Component,不是Spring Bean
public class LogService {
@Autowired
private LogMapper logMapper;
@Transactional // 完全无效,Spring不知道这个类的存在
public void saveLog(Log log) {
logMapper.insert(log);
}
}原因:Spring事务依赖Spring IoC容器来管理Bean和创建代理。如果类没有被Spring管理,就不会被创建代理,@Transactional注解形同虚设。
修复方案:添加@Service或@Component注解
@Service // 加入Spring容器
public class LogService {
@Autowired
private LogMapper logMapper;
@Transactional // 现在生效
public void saveLog(Log log) {
logMapper.insert(log);
}
}失效场景6:事务传播行为设置不当
@Service
public class ReportService {
@Transactional
public void generateReport() {
// 这里的数据库操作在事务A中
// 调用logService.log,REQUIRES_NEW会新开一个独立事务
logService.log("开始生成报告"); // 事务B
// 如果这里抛异常,事务A回滚
// 但事务B已经提交了,log记录不会回滚!
processData(); // 假设这里抛出异常
}
}@Service
public class LogService {
// REQUIRES_NEW:无论当前有没有事务,都新开一个独立事务
// 独立事务:不受外层事务影响,也不影响外层事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String message) {
logMapper.insert(new Log(message));
}
}这不一定是"失效",有时是符合预期的。但当你期望内层事务跟随外层事务一起回滚时,就不应该用REQUIRES_NEW。
各种传播行为说明:
// 最常用:默认值,如果有事务就加入,没有就新建
@Transactional(propagation = Propagation.REQUIRED)
// 总是新建独立事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
// 有事务就加入,没有就不用事务执行
@Transactional(propagation = Propagation.SUPPORTS)
// 必须在事务中执行,否则抛异常
@Transactional(propagation = Propagation.MANDATORY)
// 总是以非事务方式执行,如果有事务则挂起
@Transactional(propagation = Propagation.NOT_SUPPORTED)
// 嵌套事务:有保存点,子事务回滚不影响父事务
@Transactional(propagation = Propagation.NESTED)失效场景7:多数据源/非Spring管理的DataSource
// 配置了多个数据源,但@Transactional默认用哪个?
@Service
public class SyncService {
@Autowired
@Qualifier("masterMapper")
private UserMapper masterUserMapper; // 主库
@Autowired
@Qualifier("slaveMapper")
private OrderMapper slaveOrderMapper; // 从库
// 事务失效!@Transactional只管理了一个数据源的事务
// 跨数据源操作无法保证原子性
@Transactional
public void syncData() {
User user = masterUserMapper.findById(1L);
// 这个操作在另一个数据源上,@Transactional无法管理它
slaveOrderMapper.updateStatus(user.getId(), 1);
}
}原因:Spring的@Transactional事务管理器(PlatformTransactionManager)绑定到一个DataSource,跨数据源的操作需要分布式事务(XA或TCC等)。
修复方案:使用分布式事务框架(如Seata),或重新设计为单数据源操作,或接受最终一致性。
四、面试官追问
追问1:Spring事务的隔离级别有哪几种,和MySQL默认隔离级别的关系?
我的回答:Spring的@Transactional有五个隔离级别:DEFAULT(使用数据库默认)、READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE。MySQL默认是REPEATABLE_READ,Spring的DEFAULT就会用MySQL的REPEATABLE_READ。大多数情况下用DEFAULT就好,只有特殊业务需求才需要显式指定,比如报表统计允许脏读时可以用READ_UNCOMMITTED提高并发,但要明确知道风险。
追问2:@Transactional(readOnly = true)有什么作用?
我的回答:readOnly=true是一个优化提示,告诉数据库这是一个只读事务。MySQL会基于这个信息做一些优化,比如不记录undo log(因为不需要回滚),InnoDB不会设置行锁。同时Spring会检查readOnly=true的事务中是否有写操作,如果有则抛异常。在读写分离的架构中,readOnly=true还可以被路由到从库,减轻主库压力。
追问3:@Transactional加在接口上和加在实现类上有什么区别?
我的回答:规范上,Spring文档建议@Transactional加在具体类或方法上,而不是接口上。原因是Java的注解不能被实现类继承(接口注解不会被实现类继承),如果使用CGLIB代理(没有接口时),接口上的@Transactional不会被发现。如果用JDK代理,接口上的注解可以通过反射找到。为了避免这种平台依赖性,标准做法是把@Transactional放在实现类上。
五、同类题目举一反三
如何实现手动控制事务?
@Service
public class ManualTxService {
@Autowired
private PlatformTransactionManager transactionManager;
public void manualTransaction() {
TransactionStatus status = transactionManager.getTransaction(
new DefaultTransactionDefinition()
);
try {
// 执行业务逻辑
doWork();
transactionManager.commit(status); // 手动提交
} catch (Exception e) {
transactionManager.rollback(status); // 手动回滚
throw e;
}
}
}六、踩坑实录
坑一:支付系统事务失效导致资金不一致
这就是开篇讲的那个故事。加款方法被同类内部调用,@Transactional失效,出错时没有回滚,导致扣款成功加款失败。排查了两天。
修复后,我在代码审查规范里加了一条:同类调用包含事务注解的方法,必须通过Spring Bean调用,禁止直接用this调用。
坑二:受检异常不回滚
有个同事在做文件上传时,抛出了IOException,以为有@Transactional保护,但事务没回滚。文件存储了,数据库记录也写进去了,但因为后续操作失败,数据处于不一致状态。
根本原因:没有配置rollbackFor = Exception.class。从那以后,我们团队规定:凡是@Transactional注解,必须加rollbackFor = Exception.class,避免这类隐患。
坑三:日志Service用REQUIRES_NEW引发的问题
我们的日志Service用了REQUIRES_NEW传播行为,独立提交日志。有次核心业务失败回滚了,但日志里记录的是"业务执行成功"。原因是日志记录代码在业务代码之后,业务的回滚不影响已提交的日志事务,但业务的失败让日志内容判断错了(条件判断写在业务代码中)。
这不是Bug,但说明REQUIRES_NEW要谨慎使用,要清楚它带来的逻辑隔离性。
七、总结
Spring事务失效的7种场景,记住一个核心:一切让Spring代理失效的操作,都会导致事务失效。
速查表:
- 同类内部调用 → 走代理调用或抽出到另一个Bean
- 非public方法 → 改为public
- 异常被吞 → catch后重新抛或手动setRollbackOnly
- 受检异常不回滚 → rollbackFor = Exception.class
- 非Spring管理的类 → 加@Service/@Component
- 传播行为不当 → 根据业务选择合适的传播行为
- 多数据源 → 分布式事务或重新设计
在实际项目中,场景1(同类内部调用)和场景4(受检异常)是最最常见的,也是最容易忽视的,务必重点掌握。
