你的Spring事务为什么没回滚?这8个坑踩中一个就完蛋!

Spring事务失效的本质是:Spring的声明式事务管理是基于AOP(动态代理)实现的,任何破坏代理机制或事务上下文的行为都会导致失效。

一、核心原理回顾

Spring通过AOP代理为加了@Transactional注解的Bean注入事务管理逻辑。当你调用userService.update()时,你实际上调用的是Spring生成的代理对象的方法,而不是原始对象的方法。代理方法的结构大致如下:

// Spring 事务代理伪代码
public class UserServiceProxy extends UserService {
    private UserService target; // 原始对象

    public void update(User user) {
        TransactionStatus status = null;
        try {
            // 1. 获取事务管理器
            // 2. 开启事务(status = transactionManager.getTransaction())
            // 3. 调用原始对象的方法 target.update(user);
            // 4. 提交事务(transactionManager.commit(status))
        } catch (Exception e) {
            // 5. 判断异常是否需要回滚
            if (needRollback(e)) {
                transactionManager.rollback(status); // 回滚
            }
            throw e;
        }
    }
}

任何导致无法执行上述代理逻辑的情况,都会造成事务失效。

二、事务失效的八大场景及解决方案

1. 方法非Public修饰(最常见易错)

原因:Spring的CGLIB代理基于继承,无法代理private、protected或包级访问的方法。JDK动态代理也要求实现接口的必须是public方法。
现象:事务注解不生效,方法以非事务方式运行。
解决:确保被@Transactional注解的方法是public的。

2. 同类方法自调用(最隐蔽的坑)

  • 原因:如下所示,自调用 this.updateUserStatus() ,this 直接调用了原始对象的方法,当你从Spring容器中@Autowired这个Bean时,你拿到的是代理对象,而不是原始的目标对象。关键在于 this 指向的是原始对象而非代理对象,因此无法进入代理对象的事务,因此事务注解不会生。
  • 代码事例:
@Service
public class UserService {
    
    public void updateUser(User user) {
        // ... 一些逻辑
        this.updateUserStatus(user.getId()); // 自调用,事务失效!
    }
    
    @Transactional
    public void updateUserStatus(Long userId) {
        // 此方法的事务不会生效
        userDao.updateStatus(userId, 1);
    }
}
  • 解决方案:
    • 方案A(推荐):将updateUserStatus方法抽到另一个Service中(如UserStatusService),然后通过注入UserStatusService来调用。
@Service
public class UserService {
    // 注入另一个Service(实际上是它的代理对象)
    @Autowired
    private UserStatusService userStatusService;

    public void createUser(User user) {
        // ... 一些其他业务逻辑
        userStatusService.updateUserStatus(user.getId(), "ACTIVE"); // 通过代理对象调用
    }
}

@Service // 将事务方法拆分到另一个Service
public class UserStatusService {

    @Autowired
    private UserRepository userRepository;

    @Transactional // 现在这个注解会正常工作了
    public void updateUserStatus(Long userId, String status) {
        userRepository.updateStatus(userId, status);
    }
}```

  - 方案B:通过AopContext获取当前代理对象再调用(需开启exposeProxy)。

```java
@EnableAspectJAutoProxy(exposeProxy = true) // 启动类上加注解

((UserService) AopContext.currentProxy()).updateUserStatus(user.getId());

3. 异常类型不正确或被捕获

原因: Spring默认只在抛出运行时异常(RuntimeException) 和 Error 时回滚事务。如果抛出的是受检异常(如Exception),或者异常被方法自己try-catch吞掉了,事务不会回滚。

@Transactional
public void createOrder(Order order) {
    try {
        orderDao.save(order);
        // 模拟业务逻辑
        int i = 1 / 0; // 会抛出RuntimeException
    } catch (Exception e) {
        // 异常被捕获并处理了,代理感知不到异常,会提交事务!
        log.error("创建订单失败", e);
    }
}

解决方案:

  • 在@Transactional注解中指定rollbackFor属性:
@Transactional(rollbackFor = Exception.class) // 所有异常都回滚
  • 如果业务需要捕获异常,必须在catch块中手动抛出运行时异常:
catch (Exception e) {
    log.error("创建订单失败", e);
    throw new RuntimeException(e); // 或者自定义运行时异常
}

4. 数据库引擎不支持事务

  • 原因:如果你使用的是MySQL数据库,并且表使用的存储引擎是MyISAM,那么事务会失效,因为MyISAM根本不支持事务。
  • 解决:将表的存储引擎改为InnoDB。

5. Propagation 传播行为设置不当

  • 原因:传播行为配置错误。例如:
@Service
public class UserService {
    @Transactional(propagation = Propagation.REQUIRED)
    public void methodA() {
        // ...
        userService.methodB(); // 如果methodB的传播行为是NOT_SUPPORTED或NEVER,则会挂起或抛出异常
    }
}

@Service
public class OtherService {
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void methodB() {
        // 此方法将以非事务方式执行
    }
}
  • 解决:根据业务逻辑,正确配置传播行为。务必理解REQUIRED, REQUIRES_NEW, NESTED, NOT_SUPPORTED等行为的区别。

6. 未被Spring容器管理

  • 原因:如果你自己new了一个对象(new UserService()),然后调用其上的@Transactional方法,事务绝对不生效。因为Spring无法为这个你自己new出来的对象提供代理。

  • 解决:确保对象是通过Spring容器注入的(使用@Autowired)。

7. 多数据源下未指定事务管理器

  • 原因:项目配置了多个数据源和事务管理器,但@Transactional注解在使用时,不知道应该使用哪个事务管理器。

  • 解决:在@Transactional注解上显式指定使用哪个事务管理器:

@Transactional(transactionManager = "orderTransactionManager") // 指定事务管理器Bean名称
public void createOrder() {
    // ...
}

8. 异步方法(@Async)

原因:@Async和@Transactional都是基于AOP代理的。在一个异步方法中,事务的开启和提交是在原始线程,而实际业务逻辑的执行是在另一个线程,两者不在同一个线程上下文中,因此事务会失效。

解决:避免在异步方法中处理事务性操作。如果必须,可以考虑在异步方法内部手动编程式事务。

三、排查工具与技巧

  • 开启调试日志:在application.yml中开启相关日志,查看事务的开启、提交、回滚情况。
logging:
  level:
    org.springframework.jdbc.datasource.DataSourceTransactionManager: DEBUG
    org.springframework.orm.jpa.JpaTransactionManager: DEBUG
  • 检查代理是否生效:在IDE中Debug,查看注入的Service对象是否是类似UserServiceEnhancerBySpringCGLIBEnhancerBySpringCGLIBEnhancerBySpringCGLIB…这样的代理对象。

总结

Spring事务失效的根本原因可以归结为三点:

  • 代理失效:非public方法、自调用、未由Spring管理。

  • 异常处理不当:异常类型不对或异常被捕获。

  • 上下文环境错误:数据库不支持、传播行为配置错误、多数据源冲突、线程上下文切换。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值