前置知识点
spring事务中默认能捕获的异常要不是运行时异常或者它的子类,要不是error;但是如果是exception或者exception的子类 spring事务无法捕捉到;解决方案是在@Transactional中添加Rollback属性= Exception.class
注意: 以下带 ->后内容均是个人理解
事务失效场景
1. 方法内部调用
场景:在日常开发中,可能会遇见在某个ServiceImpl类中通过某个方法调用另外一个事务方法.这时候就会发生事务失效,因为Spring事务是通过Aop生成代理对象来完成开启和提交/回滚事务的,所以要想事务生效必须由代理对象来调用方法(事务--->Aop--->代理对象-->代理逻辑(1.开始事务 回滚 提交)).
示例:图中submitOrder方法调用saveOrderDetail()方法时,是通过this去调用的(默认隐藏this 这里显示了)也就是当前对象调用并不是代理对象,所以会导致事务失效.
@Transactional
@Override
public String submitOrder(OrderVo orderVo) throws Exception {
Order order = new Order();
order.setId(1L);
orderMapper.insert(order);
saveOrderDetail(orderVo,order);
return "提交订单成功";
}
@Transactional(rollbackFor = Exception.class)
public void saveOrderDetail(OrderVo orderVo, Order order) {
OrderDetail orderDetail = new OrderDetail();
orderDetail.setOrderId(order.getId());
orderDetail.setItemId(orderVo.getItemId());
int insert1 = orderDetailMapper.insert(orderDetail);
log.info("插入订单项操作:{}", insert1 > 0 ? "成功" : "失败");
}
3种解决方案
-
自己注入自己 ServiceImpl注入ServiceImpl 来调用方法
-
在平时开发过程中,一般是通过Service.方法()来调用ServiceImpl重写的方法也就是说ServiceImpl就是一个代理对象
-
@Slf4j @Service public class BusinessOpsServiceSupport implements BusinessOpsService { @Autowired private BusinessOpsServiceSupport businessOpsServiceSupport; @Autowired private OrderMapper orderMapper; @Autowired private OrderDetailMapper orderDetailMapper; @Autowired private TransactionBusinessService transactionBusinessService; @Transactional @Override public String submitOrder(OrderVo orderVo) throws Exception { Order order = new Order(); order.setId(1L); orderMapper.insert(order); businessOpsServiceSupport.saveOrderDetail(orderVo,order); return "提交订单成功"; } @Transactional(rollbackFor = Exception.class) public void saveOrderDetail(OrderVo orderVo, Order order) { OrderDetail orderDetail = new OrderDetail(); orderDetail.setOrderId(order.getId()); orderDetail.setItemId(orderVo.getItemId()); int insert1 = orderDetailMapper.insert(orderDetail); log.info("插入订单项操作:{}", insert1 > 0 ? "成功" : "失败"); } -
注意:
-
- 使用这种方式会导致出现循环依赖,Spring默认支持开启循环依赖,而SpringBoot项目默认是关闭的,需要在配置文件中开启
-
- spring.main.allow-circular-references=true
-
-
将该方法放入其他Service中
- 这里通过注入其他Service调用就不会导致事务失效 本质还是生成了代理对象去调
-
通过AopContent类 ->最帅的方式
- 通过aop获取当前impl的代理对象,通过代理对象去调用方法
java @Transactional @Override public String submitOrder(OrderVo orderVo) throws Exception { //通过aop获取代理对象 BusinessOpsServiceSupport supportProxy = (BusinessOpsServiceSupport)AopContext.currentProxy(); Order order = new Order(); order.setId(1L); orderMapper.insert(order); supportProxy.saveOrderDetail(orderVo,order); return "提交订单成功"; }
注意:这种写法在springboot中一定要通过注解或者配置的形式将代理对象暴露出来
后续事务传播都使用这种方式来实现方法调用,避免出现事务失效
2.事务传播行为导致失效
A.REQUIRED(默认)
-
-
官方解释:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务 ->常用
-
场景:
-
如果A、B两个方法都加了@Transactional注解,默认是REQUIRED传播行为。那么如果A方法调用B方法,它们会共用一个事务,因为默认会使用同一条连接,相当于一个事务里执行。->A,B异常都回滚
-
但是如果A方法没有,B方法有@Transactional
-
当A执行完sql并调用完B方法之后发生异常,不会导致A,B事务回滚**->A没事务,B 是新事务** 当A执行完sql并调用B方法后,B方法发生异常,只会导致B事务回滚**->A没事务,B事务检测异常回滚**
-
-
B.SUPPORTS
-
官方解释:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。->从众,摆烂
- 配置:@Transactional(propagation = Propagation.SUPPORTS)
-
场景
-
- 如果A方法存在事务(加了@Transactional注解)调B,且B方法开启了事务且配置了SUPPORTS传播行为,那么B方法会挂起自己的事务,加入到A方法的事务来执行。->A,B异常都回滚
- 如果A方法没有事务(没有加@Transactional注解),B方法开启了事务且配置了SUPPORTS传播行为,那么B方法也会以非事务方式执行。-> A没事务,B和A一致,出现异常都不回滚
-
C.MANDATORY
-
官方解释:如果当前存在事务,则加入该事务;如果当前没有事务,自己以事务的形式运行,但是会抛出异常。->抛出的是调用方不存在事务的异常 ->不摆烂 骂前任
-
配置: @Transactional(propagation = Propagation.MANDATORY)
-
场景
- 如果A方法存在事务(加@Transactional注解),B方法配置了MANDATORY传播行为,那么B方法将加入到该存在的事务来执行。->A,B异常都回滚
- 如果A方法没有事务(没有加@Transactional注解),B方法配置了MANDATORY传播行为,那么B方法将会抛出异常。但是B方法会开启一个事务来执行自己的业务。-> A没事务,B会强制开事务,B出现异常时会回滚,并抛出异常,表示A方法无事务,但A不会回滚
-
异常:org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'
D.REQUIRES_NEW
-
官方解释:创建一个新的事务,如果当前存在事务,则把当前事务挂起,在新事务提交后恢复。 ->翅膀硬了,小的犯事大的连带责任
- 挂起:将之前的数据库连接从ThreadLocal中取出来,放到一个容器中
- 恢复:在从容器中将刚刚放进去的数据库连接再次放到TreadLocal中去。
-
举例: 如图->多个sql,先执行A方法sql1,调用B方法sql2时,会将当前数据库链接放到容器中,等sql2执行完时回调,然后会将A数据库链接取出执行sql3...
-
配置:@Transactional(**propagation = Propagation.REQUIRES_NEW)
-
场景:
-
如果A方法存在事务(加@Transactional注解),B方法配置了REQUIRES_NEW传播行为,那么B方法将开启一个新的事务.->A异常,A回滚,B不回滚;但B异常,会向上抛出异常导致A一起回滚,NEW传播行为的特殊性,类似于将B方法的sql挪到A方法中
-
如果A方法没有事务(没有加@Transactional注解),B方法配置了REQUIRES_NEW传播行为。B方法会开启一个事务来执行自己的业务。-> A没事务,B有事务,B出现异常时会回滚,A不会回滚.A异常,AB都不回滚
-
E.NOT_SUPPORTED
-
官方解释:以非事务方式运行,如果当前存在事务,则把当前事务挂起。->摸鱼,摸鱼被发现 领导被处分上报/领导批评教育不上报
-
配置:@Transactional(propagation = Propagation.NOT_SUPPORTED)
-
场景:
-
- 如果A方法没有事务(没有加@Transactional注解),B方法配置了NOT_SUPPORTED传播行为,那么B方法也会以非事务方式执行。-> AB都不回滚
- 如果A方法存在事务(加了@Transactional注解),B方法配置了NOT_SUPPORTED传播行为,那么B方法会挂起当前的事务(A方法的事务),以非事务方式来执行。-> A异常,A回滚B不回滚;B异常,B不会回滚,会将异常上抛(调用方)导致A回滚/如果A捕获该异常,并做其它处理(非抛异常,如log记录)A就不会回滚
-
F.NEVER
-
官方解释:以非事务方式运行,如果当前存在事务,则抛出异常。 -> 通知: 你有,我就有
-
配置:**@Transactional(propagation = Propagation.NEVER)
-
场景:
- 如果A方法没有事务(没有加@Transactional注解),B方法配置了NEVER传播行为,那么B方法会正常以非事务方式执行。 ->AB都不会回滚
- 如果A方法存在事务(加了@Transactional注解),B方法配置了NEVER传播行为,那么B方法将会抛出异常。但是B方法会和A方法共用同一个事务。也即同一个数据库连接。->当B发生异常时会和A共用一个事务,且还会抛出这个事务是never传播行为的异常,导致AB都回滚
-
-
异常:org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'
###G.NESTED
-
官方解释:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于REQUIRED ->缝合怪
-
配置:**@Transactional(propagation = Propagation.**NESTED)
-
场景:
-
如果A方法存在事务(加@Transactional注解),B方法配置了NESTED传播行为,那么B方法将会在嵌套事务内执行。-> A,B异常都会回滚
-
和REUIRES和REQUIRES_NEW的区别在哪?
- 区别于REQUIRES_NEW(父异,子不回;子异,父回)
- 但和REQUIRED的区别是在NESTED情况下,被调用方发生异常时,调用方可以catch其异常,这样只有子事务回滚,父事务不受影响(两个事务);而Requires是同一事务,catch了也会回滚
-
-
如果A方法没有事务(没有加@Transactional注解),B方法配置了NESTED传播行为,那么B方法将启动一个新的嵌套事务执行。 -> A无事务,B开启新事务
-
-
整理方便记忆
-
-
支持现在的事务
-
- REQUIRES ->常用
- SUPPORTS ->从众,摆烂
- MANDATORY -> 不摆烂,独立骂前任
-
-
-
不支持现在的事务
-
- REQUIRES_NEW -> 翅膀硬了,小的惹事大的连带责任
- NOT_SUPPORTED -> 摸鱼,摸鱼被发现领导被处分上报/领导批评教育不上报
- NEVER -> 通知:你有,我就有
-
-
-
嵌套
-
- NESTED -> 缝合怪
-
3.访问权限问题
- 开发时一般从方法内抽取出来的方法默认为private,而要使用@Transactional必须要public
4.方法使用 final 或static 修饰
- Spring事务的底层其实是使用了AOP,也就是通过
jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能 - 但是某个方法被
final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能 - 而被static修饰的方法,属于静态方法,属于类,不属于实例对象,无法被代理
5.未被Spring管理 ->没加注解
6.多线程调用导致
- 众所周知Spring事务管理器要想对数据库进行io操作必须要从数据源拿到数据库连接才能执行操作,而拿到的数据库连接会放进ThreadLocal中
- 所以在多线程环境下会导致方法A是数据库链接1,异步B方法从ThreadLocal中拿不到1,就只有重新去源拿链接2 这就会导致不在一个事务中
- 所以我们说的同一个事务,其实指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程中,拿到的数据库连接肯定是不一样的。所以事务也是不同的。
注意:在Spring源码中Spring事务是通过数据库的连接来实现的。底层通过使用ThreadLocal来进行存储,key是当前线程,value保存的是一个Map,Map的key又是数据源,value是数据库连接。
7.MyISAM存储引擎不支持事务
8.异常被自己吃掉了
- 实际开发中,通过try.catch捕获到的异常,没有将异常抛出,选择通过log方式等打印出来不会导致事务回滚,那么如果想要Spring能够正常回滚,则必须要抛出它能够处理的异常。
-
强制回滚
-
如果想返回友好的信息给客户端,并且还想回滚 可以通过以下代码强制回滚
-
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
-
9.手动抛出了别的异常
开发中自定义的异常如果没有实现RunTimeException或Error,而是Exception(非运行时异常),不会回滚