1

1.抛出检查异常

2.业务方法内try-catch

3.切面顺序

4.非public方法,被final和static修饰的方法

5.父子容器

6.使用this调用本类方法

7.多线程下原子性失效

8.Transaction导致加锁失败

9.多线程调用

10.使用了不支持事务的存储引擎

11.错误的传播行为

1.抛出检查异常

产生原因:
我们在使用@Transactional注解的时候,如果不加rollbackFor的类型

@Transactional

它默认抛出的是RuntimeException
image.png

我们可以看一下异常的类图
image.png

也就是说,默认情况下,IOException,SQLException等异常,不会造成事务的回滚

解决方案:

@Transactional(rollbackFor = Exception.class)

指定回滚的类型为Exception.class

2.业务方法内try-catch

产生原因
业务逻辑代码内try-catch异常,直接消化异常,导致spring无法捕捉到异常。

    @Transactional(rollbackFor = Exception.class)
    public void transfer(int fromAccount, int toAccount, int money) {
        //模拟一个转账操作
        try {
            int fromBalance = userAccountMapper.findBalanceByUserAccount(fromAccount);
            if (fromBalance - money >= 0) {
                //减钱
                userAccountMapper.update(fromAccount, -1 * money);
                //增钱
                userAccountMapper.update(toAccount, money);
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

解决方案:
1.我们应该抛出异常让spring捕捉到这个错误,这样就可以进行回滚。
2.捕捉到异常后手动回滚

    @Transactional(rollbackFor = Exception.class)
    public void transfer(int fromAccount, int toAccount, int money) {
        //模拟一个转账操作
        try {
            int fromBalance = userAccountMapper.findBalanceByUserAccount(fromAccount);
            if (fromBalance - money >= 0) {
                //减钱
                userAccountMapper.update(fromAccount, -1 * money);
                //增钱
                userAccountMapper.update(toAccount, money);
            }

        } catch (Exception e) {
            e.printStackTrace();
            //throw new RuntimeException();
            TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
        }
    }

3.切面顺序

产生原因:
事务注解的切面优先级最低,如果我们自定义的切面就已经将异常消化掉,会导致事务切面无法生效。

@Aspect
@Component
@Slf4j
public class AopAspect {
    //这个切面优先级比事务切面高,先行catch异常
    @Around(value = " execution (* com.example.demo.userAccount..*.*(..))")
    public Object around(ProceedingJoinPoint pjp){
        Object result = null;
        try {
            log.info("执行前");
            result = pjp.proceed();
        } catch (Throwable throwable) {
            log.error("{}",throwable);
        }
        log.info("执行后");

        return result;
    }

}

解决方案:
同情况2
1.我们应该抛出异常,让spring捕捉到这个错误,这样就可以进行回滚。
2.捕捉到异常后手动回滚

4.非public方法,被final和static修饰的方法

产生原因:
spring为方法对象创建代理,添加切面的条件是方法必须是public的且不能被static和final修饰,不然就会导致spring无法重写该方法,导致事务失效。

解决方案
使用public修饰该方法。

5.父子容器
这种情况在springBoot的时代一般比较少遇到。

产生原因:

package tx.app.service;

// ...

@Service
public class Service5 {

    @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);
            accountMapper.update(to, amount);
        }
    }
}
package tx.app.controller;

// ...

@Controller
public class AccountController {

    @Autowired
    public Service5 service;

    public void transfer(int from, int to, int amount) throws FileNotFoundException {
        service.transfer(from, to, amount);
    }
}
@Configuration
@ComponentScan("tx.app.service")
@EnableTransactionManagement
// ...
public class AppConfig {
    // ... 有事务相关配置
}
@Configuration
@ComponentScan("tx.app")
// ...
public class WebConfig {
    // ... 无事务配置
}

原因:我们发现子容器没有事务配置但是把所有组件都扫描了进来,事务依然失效。

解决方案:

解法1:各自扫描自己的组件

解法2:不要用父子容器,所有 bean 放在同一容器

6.使用this调用本类方法
我们使用this方法调用注解的方法,会发现事务失效:

    public void transferAccount(int fromAccount, int toAccount, int money) {
        this.transfer(fromAccount,toAccount,money);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void transfer(int fromAccount, int toAccount, int money) {
        //模拟一个转账操作
        try {
            int fromBalance = userAccountMapper.findBalanceByUserAccount(fromAccount);
            if (fromBalance - money >= 0) {
                //减钱
                userAccountMapper.update(fromAccount, -1 * money);
                //增钱
                userAccountMapper.update(toAccount, money);
            }

        } catch (Exception e) {
            e.printStackTrace();
            //throw new RuntimeException();
            TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
        }
    }

产生原因
直接使用this方法调用的不是代理对象的方法,无法增强,因此会造成事务失效。

解决方案:
1.通过注入自己来获得代理对象,进行调用。
2.通过AppContext从容器中获取代理对象,进行调用。

7.多线程下原子性失效

    @Transactional(rollbackFor = Exception.class)
    public void transfer(int fromAccount, int toAccount, int money) {
        //模拟一个转账操作
        try {
            //此时查出来的余额只是当时的余额,可能操作的时候被人修改过。
            int fromBalance = userAccountMapper.findBalanceByUserAccount(fromAccount);
            if (fromBalance - money >= 0) {
                //减钱
                userAccountMapper.update(fromAccount, -1 * money);
                //增钱
                userAccountMapper.update(toAccount, money);
            }

        } catch (Exception e) {
            e.printStackTrace();
            //throw new RuntimeException();
            TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
        }
    }

产生原因
我们发现多个线程调用这个方法操作同一个账户,会发现原子性失效,因为查询出来的余额可能被其它线程改掉,导致转账失败。

解决方案:
根据需求加上分布式锁或者synchronized。

8.Transaction导致加锁失败

产生原因
在前面我们加了锁,可能是单机锁,也可能是分布式锁,但是有一点要注意的是,这个方法执行完毕之后,事务才会去提交,但是锁已经释放了。

我们看一下这个类:

DataSourceTransactionManager

image.png
这个类是在方法执行结束之后,才去提交事务,但是提交事务之前,下一个方法已经进入执行流程,加锁失败。

解决方案:
1.扩大锁的范围
2.在方法内提交事务

9.多线程调用

    @Transactional(rollbackFor = Exception.class)
    public void transfer(int fromAccount, int toAccount, int money) {
        //模拟一个转账操作
        try {
            CompletableFuture.supplyAsync(()->{
                try {
                    //此时查出来的余额只是当时的余额,可能操作的时候被人修改过。
                    int fromBalance = userAccountMapper.findBalanceByUserAccount(fromAccount);
                    if (fromBalance - money >= 0) {
                        //减钱
                        userAccountMapper.update(fromAccount, -1 * money);
                        //增钱
                        userAccountMapper.update(toAccount, money);
                    }

                } catch (Exception e) {
                    e.printStackTrace();
                }
                return null;
            }).get();
        }  catch (ExecutionException | InterruptedException e) {
            throw new RuntimeException();
        }




    }

产生原因:
spring的事务是通过数据库连接来实现,而spring将数据库连接放在threalLocal里面。同一个事务,只能使用同一个数据库连接,多线程场景下,拿到的数据库连接不同,属于不同的事务。

解决方案:
在线程里面回滚。

10.使用了不支持事务的存储引擎
产生原因:比如mysql的InnoDB支持事务,MyISAM不支持。

解决方案:使用支持事务的存储引擎

11.错误的传播行为
产生原因:

@Transactional(rollbackFor = Exception.class,propagation = Propagation.NOT_SUPPORTED)

是指如果存在事务则将这个事务挂起,并使用新的数据库连接。新的数据库连接不使用事务。

解决方案:
不使用这种方式。


苏凌峰
73 声望38 粉丝

你的迷惑在于想得太多而书读的太少。