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