Spring事务失效的几个场景及其原因

本文详细分析了Spring事务管理中常见的问题,包括检查异常不回滚、内部异常处理、切面顺序、非public方法、父子容器、本类方法调用以及同步问题,并提供了相应的解决策略,旨在确保事务的正确性和原子性。
摘要由CSDN通过智能技术生成

1. 抛出检查异常导致事务不能正确回滚

1.1 原因

  • Spring默认只会回滚非检查异常

1.2 解决方法

  • 配置rollbackFor属性使得检查异常也会回滚
  • 意思是只要这个事务方法抛出了Exception类型的异常,都会回滚事务。
@Transactional(rollbackFor = Exception.class)

2. 业务方法内使用了try-catch处理异常导致事务不能回滚

2.1 原因

  • 事务是基于环绕通知的。事务通知只有捕捉到目标方法抛出的异常,才会执行相应的回滚处理。
  • 但是目标方法内直接将异常处理了,事务通知就感知不到有异常,会认为事务正常结束。

2.2 解决方法

  • 方式一:在catch块中再将异常抛出,让事务通知能够感知到异常
  • 方式二:在catch模块中手动设置事务进行回滚
TransactionInterceptor.currentTransactionStatus().setRollbackOnly();

3. aop切面顺序导致事务不能正确回滚

@Service
public class Service3 {

    @Autowired
    private AccountMapper accountMapper;

    @Transactional(rollbackFor = Exception.class)
    public void transfer(int from, int to, int amount) throws FileNotFoundException {
        int fromBalance = accountMapper.findBalanceBy(from);
        if (fromBalance - amount >= 0) {
            accountMapper.update(from, -1 * amount);
            new FileInputStream("aaa");
            accountMapper.update(to, amount);
        }
    }
}
@Aspect
public class MyAspect {
    @Around("execution(* transfer(..))")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        LoggerUtils.get().debug("log:{}", pjp.getTarget());
        try {
            return pjp.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
            return null;
        }
    }
}

3.1 原因

  • 对于上述代码,我们自定义的通知也对事务方法进行了增强。而且在我们自定义的通知中捕捉了异常。
  • 虽然事务增强方法优先级是最低的,但是我们自定义的增强方法如果不设置优先级,那么它的优先级也是最低的。
  • 同样都是最低的优先级,Spring会选择自定义增强方法放在内层,将事务增强方法放在外层
  • 所以当内层自定义增强方法将异常捕捉了,那么外层的事务增强方法就无法感知到异常,自然也不会回滚事务

3.2 解决方法

  • 方法一:自定义增强也应该将异常抛出,而不是捕捉
  • 方法二:调整切面顺序。将自定义增强方法的优先级调高一点
    • 在MyAspect上添加@Order(Ordered.LOWEST_PRECEDENCE - 1)

4. 非public方法导致事务失效

4.1 原因

  • Spring为方法创建代理、添加事务通知的前提条件都是这个方法时public的
  • 如果这个方法不是public的,那么即使这个方法上有@Transactional注解,也不会进行事务增强

4.2 解决方法

  • 方法一:将方法该为public方法
  • 方法二:向容器中添加一个bean
@Bean
public TransactionAttributeSource transactionAttributeSource() {
    return new AnnotationTransactionAttributeSource(false);
}

5. 父子容器导致事务失效

5.1 原因

  • 配置了父子容器,但是子容器扫描的范围过大,把未加事务配置的service扫描进来了。(但只有父容器中添加了@EnableTransactionManagement注解)
  • 导致子容器调用service的时候,它会优先调用自己容器中的service实例,如果没有才会去调用父容器中service。所以它只能调用自己容器中没有开启事务增强的service了。

5.2 解决方法

  • 方式一:父子容器扫描的包尽量不要重复
  • 方式二:不使用父子容器,所有的bean放在同一个容器中(Springboot就是采用这种方式)

6. 调用本类方法导致传播行为失效

@Service
public class Service6 {

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void foo() throws FileNotFoundException {
        LoggerUtils.get().debug("foo");
        bar();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void bar() throws FileNotFoundException {
        LoggerUtils.get().debug("bar");
    }
}

6.1 原因

  • 本类方法调用本类方法,则内部调用的本类方法没有经过代理的原生方法。因此无法增强内部调用的本类方法

6.2 解决方法

  • 方法一:依赖注入自己(代理)来调用
@Service
public class Service6 {

	@Autowired
	private Service6 proxy; // 本质上是一种循环依赖

    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void foo() throws FileNotFoundException {
        LoggerUtils.get().debug("foo");
		System.out.println(proxy.getClass());
		proxy.bar();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void bar() throws FileNotFoundException {
        LoggerUtils.get().debug("bar");
    }
}
  • 方法二:通过AopContext拿到代理对象,来调用代理方法(需要则配置类上添加@EnableAspectJAutoProxy(exposeProxy = true) 注解
@Service
public class Service6 {
    
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public void foo() throws FileNotFoundException {
        LoggerUtils.get().debug("foo");
        ((Service6) AopContext.currentProxy()).bar();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void bar() throws FileNotFoundException {
        LoggerUtils.get().debug("bar");
    }
}
  • 方法三:通过CTW(编译时织入),LTW(加载时织入)实现功能增强

7. @Transactional没有保证原子行为

@Service
public class Service7 {

    private static final Logger logger = LoggerFactory.getLogger(Service7.class);

    @Autowired
    private AccountMapper accountMapper;

    @Transactional(rollbackFor = Exception.class)
    public void transfer(int from, int to, int amount) {
        int fromBalance = accountMapper.findBalanceBy(from);
        logger.debug("更新前查询余额为: {}", fromBalance);
        if (fromBalance - amount >= 0) {
            accountMapper.update(from, -1 * amount);
            accountMapper.update(to, amount);
        }
    }

    public int findBalance(int accountNo) {
        return accountMapper.findBalanceBy(accountNo);
    }
}

7.1 原因

  • 上述问题是,查询数据库和更新数据库之间不是原子操作。会导致一些逻辑错误(比如没钱了还能向其他用户转账)

7.2 解决方法

  • 方法一:在查询语句时就对查询到的结果记录加锁,防止别人对我们查询的记录进行了修改而我们还感知不到。

8. 在事务方法上加synchronized也无法解决原子性

@Service
public class Service7 {

    private static final Logger logger = LoggerFactory.getLogger(Service7.class);

    @Autowired
    private AccountMapper accountMapper;

    @Transactional(rollbackFor = Exception.class)
    public synchronized void transfer(int from, int to, int amount) {
        int fromBalance = accountMapper.findBalanceBy(from);
        logger.debug("更新前查询余额为: {}", fromBalance);
        if (fromBalance - amount >= 0) {
            accountMapper.update(from, -1 * amount);
            accountMapper.update(to, amount);
        }
    }

    public int findBalance(int accountNo) {
        return accountMapper.findBalanceBy(accountNo);
    }
}

8.1 原因

  • synchronized加锁在这里只能锁住目标方法,但是事务提交语句还是在事务增强方法中。所以还是不能保证从事务开始到事务提交这整个过程是原子性的。

8.2 解决方法

  • 将synchronized范围扩大(比如我们在controller层加synchronized)
  • 使用select ... for update在查询时加排他锁。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值