事务的传播性是Spring特有的,基于Spring AOP技术实现,原方法并不具备事务概念,只是Spring AOP给方法动态加上事务功能,目的是确保数据库的数据一致性。 程序方法之间存在调用,对应必然也会有事务的传播,如果各个方法间都加了事务,此时就更加值得探讨了,事务传播就是讨论调用者和被调用者之间的关系。

java事务传播类型

约束条件说明
REQUIRED如果没有事务则新建事务,如果已有事务则加入当前事务(1、A方法没有事务,调用有事务的B,那么会A开启一个新事务;2、A有事务,调用B,B加入当前事务)
REQUIRES_NEW新建事务,如果当前存在事务,则把当前事务挂起,开启一个新事务,新事务执行完之后再恢复当前事务(A方法有事务,调用同样有事务的B,那么B开启一个新事务,等B的事务执行完之后,再继续执行A的事务)
NESTED如果当前没有事务,则开启一个新事务,如果当前存在事务则开启一个子事务(嵌套事务),子事务不能单独提交,只能和父任务一起提交
SUPPORTS支持当前事务,如果没有事务,以非事务的方式执行
NOT_SUPPORTED以非事务方式执行,如果存在当前事务就把当前事务挂起
NEVER以非事务方式执行,如果当前存在事务就抛异常
MANDATORY使用当前事务,如果当前没有事务就抛异常

只有三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED

@Transactional的几种失效场景

一、事务没有生效

1、访问权限问题,public方法才会生效

2、方法用final或static修饰不会生效

因为spring事务底层是aop实现的,而aop是通过jdk动态代理或者cglib代理生成代理类实现的,如果方法使用了final修饰,那么在代理类中就无法重写方法,也就无法实现事务功能。同理方法设置static的话,也是无法通过动态代理去实现事务功能的。

3、同一个类中的方法直接内部调用,会导致事务失效

@Service
public class UserService {
    public void changedUserInfo(UserInfo userInfo) {
        //dosomeThing
        updateUserInfo(userInfo);
    }
    
    @Transactional(rollbackFor=Exception.class)
    public void updateUserInfo(UserInfo userInfo) {
        doSomeThing();
    }
}

在方法changedUserInfo中调用了updateUserInfo方法。一个方法能够拥有事务的功能是因为spring AOP生成了代理对象,做了增强逻辑,但是在上面类里直接调用的this对象的方法,这种方法实际上调的不是代理类里面的updateUserInfo方法,而是本身的方法,所以这样调用updateUserInfo并不会生成事务。 代码层面看原因是在InvocationHandlerImpl#invoke中method.invoke(subject, args);这里调用的是目标类subject的方法,直接执行目标类方法,不会执行代理类的方法。 如果非要在一个类中某方法调用其它加了事务的方法呢? 3.1 可以将带事务的方法抽出来写一个新的类UserServiceB,然后在UserService中注入该bean调用事务方法就行了;(在UserService类注入UserService本身也行,这样通过类去调用事务方法就可以走代理了)

public class UserService {
    @Autowired
    prvate UserService userService;
    public void changedUserInfo(UserInfo userInfo) {
        //dosomeThing
        userService.updateUserInfo(userInfo);
    }
    
    @Transactional(rollbackFor=Exception.class)
    public void updateUserInfo(UserInfo userInfo) {
        doSomeThing();
    }
} 

3.2 使用AopContext.currentProxy()获取代理对象

@Service
public class UserService {
    public void changedUserInfo(UserInfo userInfo) {
        //dosomeThing
        ((UserService)AopContext.currentProxy()).updateUserInfo(userInfo);
    }
    
    @Transactional(rollbackFor=Exception.class)
    public void updateUserInfo(UserInfo userInfo) {
        doSomeThing();
    }
}

4、类本身未注入spring IOC

别忘了使用@Controller、@Service、@Component、@Repository等注解将bean注入ioc。

5、多线程调用

public class UserService {


    @Autowired
    private RoleService roleService;

    @Transactional(rollbackFor=Exception.class)
    public void saveUserInfo(User user) throws Exception {
        //dosomeThing
        //开启新线程(一般用线程池,演示用)
        new Thread(() -> {
            roleService.saveRoleInfo(user);
        }).start();
    }
}

@Service
public class RoleService {

    @Transactional(rollbackFor=Exception.class)
    public void saveRoleInfo(User user) {
        //dosomeThing
    }
}

同一个事务必须基于同一个数据库连接,才能做到多个操作的同时提交或回滚,作为一个原子操作。UserService的saveUserInfo方法和RoleService的saveRoleInfo方法在不同的两个线程里面,是两个不同的数据库连接,所以是两个不同的事务。

6、数据库存储引擎不支持事务

mysql5之前,引擎是myisam,不支持事务。

7、没有开启事务

springboot默认开启事务,但如果你的是老spring项目,需要applicationContext.xml文件中配置开启。

二、事务没有回滚

1、错误的传播特性。

比如设置了propagation = Propagation.NEVER,这个类型就是不支持事务,有事务则抛异常。

2、【常见】异常被吞掉。

开发者在代码中自己捕获了异常,却没有抛出,spring就是根据事务方法里面抛出的异常来判断程序是否执行正常,是否需要回滚,如果try catch了异常却没有throw出来,那么spring就会认为程序是正常的,不会回滚。

3、抛出的异常不对

在第2点里是没有抛出异常,spring认为程序正常,那么如果我try catch之后throw一个异常是不是spring就一定能正常回滚了呢?不一定,spring事务默认情况只会回滚RuntimeException和Error,如果开发者catch异常之后throw了其他的异常,比如IOException或者SQLException这种普通Exception,spring事务也不会回滚。

4、自定义了回滚异常

@Transactional(rollbackFor = BusinessException.class) BusinessException是开发者自定义的异常,如果程序运行中抛的异常不属于BusinessException的范围里,那么事务也不会回滚。 注意:基于事务没有回滚的3、4点,spring事务注解@Transactional默认的回滚的异常类型是rollbackFor=RuntimeException.class,但很多时候我们的程序抛的异常不属于RuntimeException(比如SQLException、DuplicateKeyException),但也希望回滚,所以一般建议将rollbackFor设置为Exception.class或者Throwable。

5、嵌套事务回滚到了外层(回滚多了)

在嵌套事务的内层事务里面,如果出现了异常却没有捕获处理,异常向上抛到了外层,被外层事务捕获了,这种情况会回滚整个事务,如果希望只回滚内层事务而不回滚外层事务,那么可以在内层事务放在外层的try catch里面,外层将内层事务异常捕获然后“吃掉”不抛出,而保证外层事务正常执行。

三、大事务与编程式事务

1、大事务。

大事务并不少见,很多时候我们使用声明式事务都是直接在整个业务方法上面加注解,如果这个方法里面的数据库操作很多、耗时长,就是一个“大事务”,可能会引发接口超时,数据库主从延迟等问题。粒度太大,可能在方法里有几个数据库查询操作,几个更新操作,其实只有更新操作部分是需要事务特性的,但是却和查询操作也绑在一个事务里,那么怎么解决呢? 一个是将数据库更新部分逻辑抽出来一个单独的方法加事务,然后调用。另一个方法就是“编程式事务”

2、编程式事务。

具体使用案例可以看这篇博客,用spring注解开启事务叫声明式事务,spring还有一种创建事务的方式,就是通过编写代码,使用它提供的事务管理类去开启事务,也叫“编程式事务”。 2.1 相关类是TransactionTemplate,在它的核心execute方法中实现了事务的功能。 //设置事务传播性 transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); // 指定事务隔离级别(可不设置,默认是ISOLATION_DEFAULT,同数据库隔离级别)。 2.2 或者使用DefaultTransactionDefinition+DataSourceTransactionManager实现。

   @Autowired
   private TransactionTemplate transactionTemplate;
  
   public void save(final User user) {
         transactionTemplate.execute((status) => {
            //doSameThing...
            return Boolean.TRUE;
         })
   }

声明式事务虽然简便好用,但是粒度不好控制,而且得注意事务失效的情况,如果方法本身逻辑简单、不会有什么变动可以使用,熟练使用编程式事务在面对复杂点的业务时是非常有必要的。