Spring同一个Service类非事务方法调用事务方法事务失效解决方案

今天看到线上异常,根据异常排查了相关落库存储的数据,然后进而排查相关业务Service中逻辑代码,事务方法中对A、B、C三张表调用其相关Service依次插入记录。但是B插入失败,A表插入记录并没有事务回滚。然后排查了一下业务代码逻辑,发现在一个Service中,一个非事务方法调用事务方法导致的事务并没有生效导致的。

回溯

有一个Service类,其中一个成员方法假设A方法,业务逻辑就是通过调用另外一个service通过http请求获取数据,然后进行数据处理,处理完毕后,会调用该Service类的另外一个B方法进行落库存储,A方法并没有增加@Transactional,B方法增加@Transactional 注解,但是事务并没有生效。

SyncOrderProcessor

@Component
@Slf4j
public class SyncOrderProcessor {

    @Autowired
    SmpClient smpClient;

    @Autowired
    ResolverCoordinator resolverCoordinator;

    @Autowired
    OrderInfoService orderInfoService;

    @Autowired
    CustomerInfoService customerInfoService;

    @Autowired
    CarInfoMapper carInfoMapper;

    /**
     * 根据身份证查询结清订单并落库存储
     * @param param
     */
    public Result<String> handle(CrzReleaseBindVerifyDTO param){
        try {
            //1、通过HTTP调用smpClient的方法,因此涉及三方网络请求调用,并没有在该方法上增加注解
            SmpClient.Response response = smpClient.querySettledOrder(param.getCustomerIdno());
            if(!response.success()){
                throw new BizException("当前身份证号未查询到已结清订单!");
            }
            String mortgageCode = MortgageUtil.getMortgageCodeTemp();
            ApiMortgageOrderDto mortgageOrderDto = JSONObject.parseObject(JSONObject.toJSONString(response.getData()),new TypeReference<ApiMortgageOrderDto>(){});
            SyncOrderContext context = SyncOrderContext.builder()
                    .mortgageCode(mortgageCode)
                    .param(param)
                    .mortgageOrderDto(mortgageOrderDto)
                    .build();
            //2、调用resolverCoordinator#execute处理数据,然后组装落库数据entity
            resolverCoordinator.execute(context);
            //3、在这里调用该Service类中的另外一个支持事务处理的方法
            persistent(context);
            return Result.suc(mortgageCode);
        } catch (InvokeException e) {
            log.error("[调用三方车融租接口异常],param={}",JSONObject.toJSONString(param),e);
            return Result.fail(RemoteEnum.ERROR_IN_SERVER,e.getMessage());
        } catch (Exception e) {
            log.error("[根据身份证查询结清订单并落库存储]异常,param={}",JSONObject.toJSONString(param),e);
            return Result.fail(RemoteEnum.ERROR_IN_SERVER,"根据身份证查询结清订单并落库存储异常");
        }
    }

    /**
     * 持久化数据
     * @param context
     */
    @Transactional(rollbackFor = Exception.class)
    void persistent(SyncOrderContext context){
        //初始化订单信息
        orderInfoService.insertRecord(context.getOrderInfo());
        //初始化客户信息
        customerInfoService.insertRecord(context.getCustomerInfo());
        //初始化车辆信息
        carInfoMapper.insert(context.getCarInfo());
    }

}

从上面代码可以看出:

handle(CrzReleaseBindVerifyDTO param)中,业务逻辑是

1、通过HTTP调用smpClient的方法,由于涉及三方网络请求调用,并没有在该方法上增加注解。

2、调用resolverCoordinator#execute处理数据,然后组装entity实体对象封装到Context上下文中进行落库数据。

3、在这里调用该Service类中的另外一个支持事务处理的方法。

出现故障是:

orderInfoService.insertRecord 调用该方法入库成功,调用customerInfoService.insertRecord方法由于列字段太长导致失败,但是事务并没有回滚,导致orderInfoService.insertRecord执行的数据落库存储了。

分析

同一个Service类中非事务方法调用事务方法,事务会失效失效,这里简单解释一下原因:spring采用动态代理机制来实现事务控制,而动态代理最终都是要调用原始对象的,而原始对象在去调用方法时,是不会再触发代理了!可以理解为同一个类中非事务方法调用方法时用的是当前对象去调用,而不是spring生成的代理对象,所以会导致事务失效。

方案

方案一:对事务方法提取一个Service

SyncOrderProcessor

@Component
@Slf4j
public class SyncOrderProcessor {

    @Autowired
    SmpClient smpClient;

    @Autowired
    ResolverCoordinator resolverCoordinator;

    @Autowired
    SyncOrderPersistentService syncOrderPersistentService;
    /**
     * 根据身份证查询结清订单并落库存储
     * @param param
     */
    public Result<String> handle(CrzReleaseBindVerifyDTO param){
        try {
            SmpClient.Response response = smpClient.querySettledOrder(param.getCustomerIdno());
            if(!response.success()){
                throw new BizException("当前身份证号未查询到已结清订单!");
            }
            String mortgageCode = MortgageUtil.getMortgageCodeTemp();
            ApiMortgageOrderDto mortgageOrderDto = JSONObject.parseObject(JSONObject.toJSONString(response.getData()),new TypeReference<ApiMortgageOrderDto>(){});
            SyncOrderContext context = SyncOrderContext.builder()
                    .mortgageCode(mortgageCode)
                    .param(param)
                    .mortgageOrderDto(mortgageOrderDto)
                    .build();
            resolverCoordinator.execute(context);
            syncOrderPersistentService.persistent(context);
            return Result.suc(mortgageCode);
        } catch (InvokeException e) {
            log.error("[调用三方车融租接口异常],param={},message={}",JSONObject.toJSONString(param),e.getMessage());
            return Result.fail(RemoteEnum.ERROR_IN_SERVER,e.getMessage());
        } catch (Exception e) {
            log.error("[根据身份证查询结清订单并落库存储]异常,param={}",JSONObject.toJSONString(param),e);
            return Result.fail(RemoteEnum.ERROR_IN_SERVER,"根据身份证查询结清订单并落库存储异常");
        }
    }

}

通过提取SyncOrderPersistentService这个类,然后注入上面类中,调用其事务方法

@Service
public class SyncOrderPersistentService {

    @Autowired
    OrderInfoService orderInfoService;

    @Autowired
    CustomerInfoService customerInfoService;

    @Autowired
    CarInfoMapper carInfoMapper;

    /**
     * 持久化数据
     * @param context
     */
    @Transactional(rollbackFor = Exception.class)
    public void persistent(SyncOrderContext context){
        //初始化订单信息
        orderInfoService.insertRecord(context.getOrderInfo());
        //初始化客户信息
        customerInfoService.insertRecord(context.getCustomerInfo());
        //初始化车辆信息
        carInfoMapper.insert(context.getCarInfo());
    }
}

这种弊端,就是会额外引入一个类,需要基于当前代码稍微重构一下代码。

方案二:通过从ApplicationContext获取当前Service对象

@Component
@Slf4j
public class SyncOrderProcessor {

    //通过增加成员变量当前service
    SyncOrderProcessor self;

    @Autowired
    ApplicationContext applicationContext;

    @Autowired
    SmpClient smpClient;

    @Autowired
    ResolverCoordinator resolverCoordinator;

    @Autowired
    OrderInfoService orderInfoService;

    @Autowired
    CustomerInfoService customerInfoService;

    @Autowired
    CarInfoMapper carInfoMapper;

    @PostConstruct
    void init(){
        //从Bean容器中获取当前实例,本质上还是Spring动态代理对象
        self = applicationContext.getBean(SyncOrderProcessor.class);
    }
    /**
     * 根据身份证查询结清订单并落库存储
     * @param param
     */
    public Result<String> handle(CrzReleaseBindVerifyDTO param){
        try {
            SmpClient.Response response = smpClient.querySettledOrder(param.getCustomerIdno());
            if(!response.success()){
                throw new BizException("当前身份证号未查询到已结清订单!");
            }
            String mortgageCode = MortgageUtil.getMortgageCodeTemp();
            ApiMortgageOrderDto mortgageOrderDto = JSONObject.parseObject(JSONObject.toJSONString(response.getData()),new TypeReference<ApiMortgageOrderDto>(){});
            SyncOrderContext context = SyncOrderContext.builder()
                    .mortgageCode(mortgageCode)
                    .param(param)
                    .mortgageOrderDto(mortgageOrderDto)
                    .build();
            resolverCoordinator.execute(context);
            //》》》》》》》注意:::这里通过self,调用其成员方法persistent
            self.persistent(context);
            return Result.suc(mortgageCode);
        } catch (InvokeException e) {
            log.error("[调用三方车融租接口异常],param={},message={}",JSONObject.toJSONString(param),e.getMessage());
            return Result.fail(RemoteEnum.ERROR_IN_SERVER,e.getMessage());
        } catch (Exception e) {
            log.error("[根据身份证查询结清订单并落库存储]异常,param={}",JSONObject.toJSONString(param),e);
            return Result.fail(RemoteEnum.ERROR_IN_SERVER,"根据身份证查询结清订单并落库存储异常");
        }
    }

    /**
     * 持久化数据
     * @param context
     */
    @Transactional(rollbackFor = Exception.class)
    public void persistent(SyncOrderContext context){
        //初始化订单信息
        orderInfoService.insertRecord(context.getOrderInfo());
        //初始化客户信息
        customerInfoService.insertRecord(context.getCustomerInfo());
        //初始化车辆信息
        carInfoMapper.insert(context.getCarInfo());
    }

}

上面的解决思路实现如下:

1、通过在该Service中增加成员变量,还是该Service的一个实例对象。

2、通过 @PostConstruct装饰一个方法,然后从ApplicationContext中获取当前ServiceBean实例。

3、调用事务方法是,则通过调用Service实例对象调用其成员方法。

方案三:启用@EnableAspectJAutoProxy(exposeProxy = true)

// 步骤一:在Application类上增加如下注解,声明启用AspectJ代理。
@EnableAspectJAutoProxy(exposeProxy = true)

// 步骤二:Process类调用成员方法修改成如下,通过从AopContext.currentProxy()获取当前代理类,进而调用其方法
((SyncOrderProcessor)AopContext.currentProxy()).persistent(context);

方案四:使用JDK+8特性基于Supplier接口封装

TransactionHandler

该类的主要作用,通过函数方法体作为参数,然后在该类中增加@Transactional,这样对于调用事务方法则无需增加注解,统一在该Handler类中处理。

@Service
public class TransactionHandler {

    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
    public <T> T runInTransaction(Supplier<T> supplier) {
        return supplier.get();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
    public <T> T runInNewTransaction(Supplier<T> supplier) {
        return supplier.get();
    }
}

然后把该类注入到Processor中,handle方法中,调用方法修改成如下

transactionHandler.runInTransaction(() -> {
    persistent(context);
    return true;
});
@Component
@Slf4j
public class SyncOrderProcessor {

    @Autowired
    ApplicationContext applicationContext;

    @Autowired
    SmpClient smpClient;

    @Autowired
    ResolverCoordinator resolverCoordinator;

    @Autowired
    OrderInfoService orderInfoService;

    @Autowired
    CustomerInfoService customerInfoService;

    @Autowired
    CarInfoMapper carInfoMapper;

    @Autowired
    TransactionHandler transactionHandler;

    /**
     * 根据身份证查询结清订单并落库存储
     * @param param
     */
    public Result<String> handle(CrzReleaseBindVerifyDTO param){
        try {
            SmpClient.Response response = smpClient.querySettledOrder(param.getCustomerIdno());
            if(!response.success()){
                throw new BizException("当前身份证号未查询到已结清订单!");
            }
            String mortgageCode = MortgageUtil.getMortgageCodeTemp();
            ApiMortgageOrderDto mortgageOrderDto = JSONObject.parseObject(JSONObject.toJSONString(response.getData()),new TypeReference<ApiMortgageOrderDto>(){});
            SyncOrderContext context = SyncOrderContext.builder()
                    .mortgageCode(mortgageCode)
                    .param(param)
                    .mortgageOrderDto(mortgageOrderDto)
                    .build();
            resolverCoordinator.execute(context);
            transactionHandler.runInTransaction(() -> {
                persistent(context);
                return true;
            });
            return Result.suc(mortgageCode);
        } catch (InvokeException e) {
            log.error("[调用三方车融租接口异常],param={},message={}",JSONObject.toJSONString(param),e.getMessage());
            return Result.fail(RemoteEnum.ERROR_IN_SERVER,e.getMessage());
        } catch (Exception e) {
            log.error("[根据身份证查询结清订单并落库存储]异常,param={}",JSONObject.toJSONString(param),e);
            return Result.fail(RemoteEnum.ERROR_IN_SERVER,"根据身份证查询结清订单并落库存储异常");
        }
    }

    /**
     * 持久化数据
     * @param context
     */
    public void persistent(SyncOrderContext context){
        //初始化订单信息
        orderInfoService.insertRecord(context.getOrderInfo());
        //初始化客户信息
        customerInfoService.insertRecord(context.getCustomerInfo());
        //初始化车辆信息
        carInfoMapper.insert(context.getCarInfo());
    }

}

扩展阅读:https://stackoverflow.com/questions/3423972/spring-transaction-method-call-by-the-method-within-the-same-class-does-not-wo

https://www.cnblogs.com/foreveravalon/p/8653832.html

  • 2
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值