一、事务
事务管理相信大家并不陌生,作为系统开发中必不可少的一部分,Spring提供了很好事务管理机制,主要分为编程式事务和声明式事务两种:
1、编程式事务:是指在代码中手动进行管理事务的提交、回滚等操作,代码侵入性比较强,如下示例:
@Resource
private TransactionTemplate transactionTemplate;
@Resource
private UserInfoMapper userInfoMapper;
@Transactional
public RespBody test(){
UserInfo userInfo = new UserInfo();
userInfo.setMuser(100100L);
transactionTemplate.execute((status)->{
try {
userInfoMapper.insertSelective(userInfo);
userInfoMapper.updateUser(79L);
}catch (Exception e){
status.setRollbackOnly();
}
return Boolean.TRUE;
});
return RespBody.success();
}
2、声明式事务:基于AOP面向切面的,它将具体业务与事务处理部分解耦,代码侵入性很低,这种在实际开发中声明式事务用得比较多。声明式事务也有两种实现方式,一种是基于TX和AOP的xml配置文件方式,第二种就是基于@Transactional注解。
二、常见的几种事务不生效场景
1、@Transactional应用在非public修饰的方法上(低级错误)
在Spring AOP 代理时,TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute方法,获取Transactional 注解的事务配置信息。此方法会检查目标方法的修饰符是否为 public,不是 public则不会获取@Transactional 的属性配置信息。
protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
}
注意:@Transactional应用在非public修饰的方法上,注解失效。protected、private或者 package-visible修饰的方法上使用 @Transactional 注解,虽然事务无效,但不会有任何报错。
2、同一个类中进行方法调用,导致@Transactional失效
在同一个类TestService中,一个被@Transactional的方法B被没有@Transactional方法A调用时(不论方法B是被public修饰还是被private修饰),都会导致方法B@Transactional作用失效。这是开发过程中最容易出现的情况。
这是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。
@Resource
private UserInfoMapper userInfoMapper;
public RespBody testA(){
testB();
return RespBody.success();
}
@Transactional
public RespBody testB(){
UserInfo userInfo = new UserInfo();
userInfo.setMuser(100100L);
userInfoMapper.insertSelective(userInfo);
userInfoMapper.updateUser(79L);
return RespBody.success();
}
处理方法:将带有事务的方法B代码移至另外一个类TransactionalService中,代码如下:
/**
* @description: 事务方法类
*/
@Slf4j
@Service
public class TransactionalService {
@Resource
private UserInfoMapper userInfoMapper;
@Transactional
public RespBody testB() {
UserInfo userInfo = new UserInfo();
userInfo.setMuser(100100L);
userInfoMapper.insertSelective(userInfo);
userInfoMapper.updateUser(79L);
return RespBody.success();
}
}
拓展场景:在实际场景当中,我们经常会遇到批量处理业务时,单个业务失败时不影响程序往下执行,但是又想那些失败的业务数据可以回滚,这个时候我们可以在事务方法里面进行异常捕获,再进行手动回滚,errList存储失败业务记录,方便之后进行补偿。代码如下:
/**
* @description: 测试方法类
*/
@Service
public class TestService {
@Resource
private TransactionalService transactionalService;
public RespBody testA(){
List<Integer> errList = new ArrayList<>();
for (int i = 0; i <5; i++) {
transactionalService.testB(errList,i);
}
return RespBody.success();
}
}
/**
* @description: 事务方法类
*/
@Slf4j
@Service
public class TransactionalService {
@Resource
private UserInfoMapper userInfoMapper;
@Transactional
public void testB(List<Integer> errList,Integer index) {
try {
UserInfo userInfo = new UserInfo();
userInfo.setMuser(100100L);
userInfoMapper.insertSelective(userInfo);
userInfoMapper.updateUser(79L);
}catch (Exception e){
log.error("这里出错啦~~",e);
errList.add(index);
//出错回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
}
3、“catch” 吃掉了@Transactional注解,导致事务失效
场景:方法A,方法C均使用了@Transactional注解,方法A调用了方法C,并且方法C在方法A的try...catch方法体内,代码如下:
/**
* @description: 测试方法类
*/
@Service
public class TestService {
@Transactional
public RespBody testA(){
try {
userInfoMapper.updateUser(79L);
transactionalService.testC();
}catch (Exception e){
e.printStackTrace();
}
return RespBody.success();
}
}
/**
* @description: 事务方法类
*/
@Slf4j
@Service
public class TransactionalService {
@Transactional
public void testC() {
UserInfo userInfo = new UserInfo();
userInfo.setMuser(100100L);
userInfoMapper.insertSelective(userInfo);
userInfoMapper.updateUser(79L);
}
}
这个时候若方法C执行报错产生异常,而A方法try catch了方法C的异常,方法C的事务不会生效,自然也无法出错回滚。
因为当方法C中抛出了一个异常以后,方法C标识当前事务需要rollback。但是方法A中由于你手动的捕获这个异常并进行处理,方法A认为当前事务应该正常commit。此时就出现了前后不一致,也就是因为这样,抛出了前面的UnexpectedRollbackException异常。
spring的事务是在调用业务方法之前开始的,业务方法执行完毕之后才执行commit or rollback,事务是否执行取决于是否抛出runtime异常。如果抛出runtime exception 并在你的业务方法中没有catch到的话,事务会回滚。
在业务方法中一般不需要catch异常,如果非要catch一定要抛出throw new RuntimeException(),或者注解中指定抛异常类型@Transactional(rollbackFor=Exception.class),否则会导致事务失效,数据commit造成数据不一致。