@Transactional和普通自定义切面执行顺序的思考
携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第20天,点击查看活动详情
前言
因为笔者之前读过spring transaction相关源码(可见《Transaction Management源码阅读路径》),所以对@Transactional和普通自定义切面执行顺序有一定的了解,本篇文章主要通过源码的角度解释下两个切面的执行顺序以及平时开放中需要注意的问题。
- 平时使用切面去加分布式锁,是先开启事务还是先尝试获得锁?这两者有啥区别?
- 业务中怎么控制切面的顺序?切面的顺序对事务的影响怎么避免?
普通未指定order的切面和@Transaction的先后顺序
先说下笔者为啥会考虑到这个,我们可以知道@Transaction一般加在具体要执行业务的service方法上,那如果我要进行并发控制对业务进行加锁,那么尝试锁和开启事务孰先孰后呢?按照业务流程上来看我们需要先尝试锁后开启事务,因为没获得锁开启事务需要和数据库进行交互开启一个新的事务,平常对业务结果是不会影响的,但是当高并发时是会对数据库带来不小压力。
但是平时的时候真的有注意和确定两者的顺序吗?下面我们来通过源码来看看。
先定义普通的切面
package com.study.spring.aspect;
import com.study.spring.annotation.SimpleAnnotation;
import com.study.spring.service.impl.TestServiceImpl;
import com.study.spring.entity.TestDo;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Slf4j
@Component
public class SimpleAspect {
@Autowired
private TestServiceImpl testService;
@Around("@annotation(simpleAnnotation)")
@SneakyThrows
public Object simpleAround(ProceedingJoinPoint pjp, SimpleAnnotation simpleAnnotation) {
log.info("进入simpleAround");
Object proceed = pjp.proceed();
log.info("完成simpleAround");
return proceed;
}
}
再在service方法中加上@SimpleAnnotation和@SimpleAnnotation
@Override
@SimpleAnnotation
@Transactional(rollbackFor = Exception.class)
public void test() {
log.info("执行业务");
TestDo testUpdate = new TestDo();
testUpdate.setId(1);
testUpdate.setName("执行业");
testService.updateById(testUpdate);
}
下面我们直接来看看生成代理时两个切面的顺序。
有两个关键点我们需要注意:
- 在APC中所有拿到的advisors会进行排序,根据order数字越大优先级越低越在数组后面越会先执行。可以看到ExposeInvocationInteceptor在最前面它是一个特殊的advisor是为切面服务的用于暴露invocation,它的order为Integer.MIN优先级最大。
- 自定义的切面(默认的是Integer.MAX优先级最低)和事务切面(默认的是Integer.MAX优先级最低)优先级是一样的,但是自定义的排在后面会后执行,因为spring扫描的时候会先扫描事务相关的。
下面看下执行顺序:
会先执行事务切面
后面执行自定义切面。
总结
如果普通切面没指定order会比transaction后执行。当锁或者一些检查性切面被使用时如果条件不满足不能进入业务也会导致事务的开启产生了不必要的消耗,当并发高时尤为明显。
如果是synchronized等阻塞性锁还会导致提前创建事务因为mvcc会导致读旧值的情况,并发时会出现问题。(可参考“事务篇(三):分享一个隐性事务失效场景”,但是文章中的例子不需要过多关注。)
那么我们怎么避免此类影响呢?
切面的顺序对事务的影响怎么避免?
其实避免方式有三种,一种是指定order,一种是把自定义切面移到更外层中,一种是使用编程式事务。
指定order
package com.study.spring.aspect;
import com.study.spring.annotation.SimpleAnnotation;
import com.study.spring.service.impl.TestServiceImpl;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Slf4j
@Component
@Order(1)
public class SimpleAspect {
@Autowired
private TestServiceImpl testService;
@Around("@annotation(simpleAnnotation)")
@SneakyThrows
public Object simpleAround(ProceedingJoinPoint pjp, SimpleAnnotation simpleAnnotation) {
log.info("进入simpleAround");
Object proceed = pjp.proceed();
log.info("完成simpleAround");
return proceed;
}
}
这里我们指定了@Order(1),下面看看APC中的advisors中的顺序。
再看看实际执行顺序
可以看到自定义的切面先执行了。
移到更外层中
移到更外层中就不用证明了,调用的自然顺序,比如放在Controller的方法上。
使用编程式事务
当然可以,调用的自然顺序,事务的开启更加现式。
总结
因为声明式事务比较好用,生产中使用的比较多,只有为了控制事务粒度或者不需要抽出一个新的类(为了使事务生效)才会使用编程式事务。
所以笔者更加倾向于移到更外层,因为指定order的前提是你知道事务切面的和不指定order普通切面的顺序,同时一旦切面变多比如有统一加锁切面、统一检查是否认证切面等需要控制自定义切面顺序容易和事务切面搞混,不利于维护,这个也相当于自定义切面和框架前面隔离。这也从一个侧面证明了校验放controller的合理性。
转载自:https://juejin.cn/post/7133207925593489439