欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

spring事务异常回滚使用注意点

程序员文章站 2022-01-15 11:57:34
...

最近写了一个后台定时任务用于自动扣款,测试时还好好的,上线后第一次执行处理也没问题,到第二次执行时,发现并没有生成数据,一开始以为是Redis判断时出了问题,导致后面的方法没执行,但是查询线上的redis相关日期key的value,发现是正确的。

 

定时任务的方法代码如下:

    /**
     * 自动收款第一次 <br>
     * 每天15点触发
     */
    @Scheduled(cron = "0 0 15 * * ?")
    public void autoCollectCashDay1() {
        log.info("开始执行自动收款任务 generateAccountStatisticsDay1 于" + DateUtils.localDateTime2Str(LocalDateTime.now(), DateUtils.TIMESTAMP));
        // 启动时检查配置,判断是否启用后台任务

        boolean isCanDoDayTask = RedisUtil.canDoDayTask(valueOperations, RedisKey.getServiceCashCollectionDay1Flag(), RedisKey.getServiceCashCollectionDay1Server());
        log.info("判断能否执行自动收款任务 generateAccountStatisticsDay1 结果:" + isCanDoDayTask);
        if (isCanDoDayTask) {
            cashCollectionOrderService.autoCollectCash(null, null);
        }
        log.info("结束执行自动收款任务 generateAccountStatisticsDay1 task 于" + DateUtils.localDateTime2Str(LocalDateTime.now(), DateUtils.TIMESTAMP));
    }

后来在代码里添加了日志再排查,查询日志里面isCanDoDayTask的值是true,说明autoCollectCash方法实际执行了,但出于某种原因出错,导致实际没产生数据,从这种现象看第一怀疑的就是事务被回滚了。

 

排查代码调用路径和日志,发现是内部调用的某个查询方法事务管理采用的默认的类上定义的回滚策略:

@Service
@Transactional(rollbackFor = Exception.class)
public class AccountService {
    ....

    public Integer findAccountId(Integer targetId, AccountBindType bindType) throws CoreException {

        if (bindType == null) {
            throw new CoreException(ReturnCode.Account.ACCOUNT_BIND_TYPE_IS_NULL, "账户绑定类型为空");
        }
        if (targetId == null) {
            throw new CoreException(ReturnCode.Account.ACCOUNT_BIND_TARGET_ID_IS_NULL, "账户绑定的目标id为空");
        }
        return accountBindMapper.findAccountId(targetId, bindType);
    }
}

使得某条数据处理时调用该查询方法的地方在事务管理上下文中标记为rollback:

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:720)
    at org.springframework.test.context.transaction.TransactionalTestExecutionListener$TransactionContext.endTransaction(TransactionalTestExecutionListener.java:597)
    at org.springframework.test.context.transaction.TransactionalTestExecutionListener.endTransaction(TransactionalTestExecutionListener.java:296)
    at org.springframework.test.context.transaction.TransactionalTestExecutionListener.afterTestMethod(TransactionalTestExecutionListener.java:189)
    at org.springframework.test.context.TestContextManager.afterTestMethod(TestContextManager.java:404)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:91)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)

这个问题好处理,事务管理改成supports就行:

    @Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
    public Integer findAccountId(Integer targetId, AccountBindType bindType) throws CoreException {

        if (bindType == null) {
            throw new CoreException(ReturnCode.Account.ACCOUNT_BIND_TYPE_IS_NULL, "账户绑定类型为空");
        }
        if (targetId == null) {
            throw new CoreException(ReturnCode.Account.ACCOUNT_BIND_TARGET_ID_IS_NULL, "账户绑定的目标id为空");
        }
        return accountBindMapper.findAccountId(targetId, bindType);
    }

 

到了下午又是类似的情况出现,表里面继续没有记录,跟踪日志发现是抛了业务数据异常,导致整个Service处方法回滚,这里异常是应该抛掉,并且这条业务数据处理就是不应该成功,但不应该影响别的业务数据处理。所以,我最开始的改动是把每条数据处理单独抽成一个方法,并标记事务处理类型为requires_new,试图通过在子方法层面单独另启一个事务使得本次业务处理不影响循环中下一次的业务。

 

旧代码:

@Service
@Transactional(rollbackFor = Exception.class)
public class CashCollectionOrderService {
...

//这里采用类上配置的事务处理策略,即rollbackFor = Exception.class
private void business(Order collectionOrder){
...
}

//这里采用类上配置的事务处理策略,即rollbackFor = Exception.class
public void autoCollectCash(LocalDateTime startTime, LocalDateTime endTime) {
        Map<Integer, List<CashCollectionOrder>> map = getGroupUnfinishedList(startTime, endTime);
        Set<Integer> storeManagerUserIds = map.keySet();
        if (!storeManagerUserIds.isEmpty()) {
            for (Integer storeManagerUserId : storeManagerUserIds) {
                List<CashCollectionOrder> list = map.get(storeManagerUserId);
                if ((list != null) && (!list.isEmpty())) {
                    //将集合按日期升序排列
                    Collections.sort(list, (CashCollectionOrder order1, CashCollectionOrder order2) -> order1.getUploadDate().compareTo(order2.getUploadDate()));
                    for (int i = 0; i < list.size(); i++) {
                        CashCollectionOrder collectionOrder = list.get(i);
                        business(collectionOrder);
                    }
                 }
             }
         }
}

改成:

@Service
@Transactional(rollbackFor = Exception.class)
public class CashCollectionOrderService {
...

@Transactional(propagation = Propagation.REQUIRES_NEW)
private void business(Order collectionOrder){
...
}

//这里采用类上配置的事务处理策略
public void autoCollectCash(LocalDateTime startTime, LocalDateTime endTime) {
        Map<Integer, List<CashCollectionOrder>> map = getGroupUnfinishedList(startTime, endTime);
        Set<Integer> storeManagerUserIds = map.keySet();
        if (!storeManagerUserIds.isEmpty()) {
            for (Integer storeManagerUserId : storeManagerUserIds) {
                List<CashCollectionOrder> list = map.get(storeManagerUserId);
                if ((list != null) && (!list.isEmpty())) {
                    //将集合按日期升序排列
                    Collections.sort(list, (CashCollectionOrder order1, CashCollectionOrder order2) -> order1.getUploadDate().compareTo(order2.getUploadDate()));
                    for (int i = 0; i < list.size(); i++) {
                        CashCollectionOrder collectionOrder = list.get(i);
                        business(collectionOrder);
                    }
                 }
             }
         }
}

但是实际结果,抛出异常后还是回滚。查资料说Transactional必须修饰在public方法上,其他类型方法虽然不会报错,但事务配置不会起作用,那么business()改成了public之后呢,还是回滚了。。。这里的原因是调用了别动业务模块的业务方法,而这些方法配置的是rollbackFor = Exception.class,这样使得出现Exception后会给整个service的方法层面添加了rollback标记,在service方法执行完由容器提交事务的时候还是把事务回滚了。尼玛,service层搞不定,只好放大招了,就把这里循环处理数据的方法提到Action层,单条数据的处理的子方法再放到service里吧。因为spring的事务针对的是service层,事务回滚提交也是注入在service的方法层切面上,这样强制把业务拆分,总算可以了。。。

 

最终代码:

    /**
     * 自动收款第一次 <br>
     * 每天15点触发
     */
    @Scheduled(cron = "0 0 15 * * ?")
    public void autoCollectCashDay1() {
        log.info("开始执行自动收款任务 generateAccountStatisticsDay1 于" + DateUtils.localDateTime2Str(LocalDateTime.now(), DateUtils.TIMESTAMP));
        // 启动时检查配置,判断是否启用后台任务

        boolean isCanDoDayTask = RedisUtil.canDoDayTask(valueOperations, RedisKey.getServiceCashCollectionDay1Flag(), RedisKey.getServiceCashCollectionDay1Server());
        log.info("判断能否执行自动收款任务 generateAccountStatisticsDay1 结果:" + isCanDoDayTask);
        if (isCanDoDayTask) {
            autoCollectCash(null, null);
        }
        log.info("结束执行自动收款任务 generateAccountStatisticsDay1 task 于" + DateUtils.localDateTime2Str(LocalDateTime.now(), DateUtils.TIMESTAMP));
    }


    /**
     * 自动收款
     *
     * @param startTime 要处理的收款单的起始时间,作为收款单的查询条件
     * @param endTime   要处理的收款单的结束时间,作为收款单的查询条件
     */
    private void autoCollectCash(LocalDateTime startTime, LocalDateTime endTime) {
        Map<Integer, List<CashCollectionOrder>> map = cashCollectionOrderService.queryGroupUnfinishedList(startTime, endTime);
        Set<Integer> orgIds = map.keySet();
        if (!orgIds.isEmpty()) {
            for (Integer orgId : orgIds) {
                AuthOrg authOrg = cashCollectionOrderService.findAuthOrgById(orgId);
                //TODO 目前只处理直营店的收款,后面要增加加盟商的处理
                if (authOrg == null || authOrg.getOrgType() != UserConstants.AuthOrgType.DIRECT_SALE) {
                    continue;
                }

                List<CashCollectionOrder> list = map.get(orgId);
                if ((list != null) && (!list.isEmpty())) {
                    //将集合按日期升序排列
                    Collections.sort(list, (CashCollectionOrder order1, CashCollectionOrder order2) -> order1.getUploadDate().compareTo(order2.getUploadDate()));

                    for (int i = 0; i < list.size(); i++) {
                        CashCollectionOrder collectionOrder = list.get(i);
                        if (collectionOrder != null) {
                            try {
//这里再调用service层的方法                                cashCollectionOrderService.collectionCash(collectionOrder, authOrg.getOrgType());
                            } catch (Exception e) {
                                log.error("收款单处理异常, id : {}, message, ", collectionOrder.getId(), e.getMessage());
                            }
                        }
                    }

                }
            }
        }
    }