`
Tyrion
  • 浏览: 261365 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

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

    博客分类:
  • Java
 
阅读更多

最近写了一个后台定时任务用于自动扣款,测试时还好好的,上线后第一次执行处理也没问题,到第二次执行时,发现并没有生成数据,一开始以为是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());
                            }
                        }
                    }

                }
            }
        }
    }

 

1
1
分享到:
评论
1 楼 hbxflihua 2017-05-21  
老弟,你这个问题应该是pring AOP代理不支持类内部方法调用导致的,Spring官方也不推荐使用类内部方法相互调用,一个是代理对象、一个是目标对象,Spring没办法对目标对象进行事务切面处理。有很多成熟的方案可以解决这个问题,比如开启AOP代理ThreadLocal支持或者Service中声明一个自我的引用对象,通过这个自我引用对象(代理对象)开启事务,还有一个方案就是通过实现BeanPostProcessor 接口取得代理对象。你的这个解决方案其实只是规避了这个问题,并没有解决问题,在批量处理的场合(比如定时任务)是不大适用的。

相关推荐

    spring 简单实例 事务回滚

    总之,这个“spring简单实例 事务回滚”案例为我们提供了一个学习Spring事务管理的好起点。通过理解如何配置事务管理器,使用`@Transactional`注解,以及异常处理机制,我们可以更好地掌握Spring如何保证数据的一致...

    子线程任务发生异常,主线程事务如何回滚

    子线程任务发生异常,主线程事务如何回滚? 本文将详细探讨当子线程任务发生异常时,如何让主线程捕获到该异常并进行事务的回滚。...在主线程中,我们可以使用 try-catch 语句来捕获异常,并进行事务的回滚。

    spring事务异常回滚实例解析

    本文主要关注的是声明式事务管理,尤其是涉及到事务异常回滚的实例解析。 首先,Spring 默认只有在遇到未捕获的 `RuntimeException` 或其子类时才会触发事务回滚。这意味着,如果在业务代码中对异常进行了捕获并...

    Spring/SpringMVC/MyBatis整合+事务回滚

    7. **实现事务回滚**:当在@Transactional注解的方法中发生异常时,Spring会自动回滚事务。例如,如果在保存数据时发生错误,整个事务将被回滚,保证数据的一致性。 8. **测试与调试**:完成整合后,通过编写单元...

    Spring事务管理只对出现运行期异常进行回滚

    5. **事务的回滚规则**:除了默认仅对运行时异常回滚外,还可以自定义回滚规则,比如通过`@Transactional(rollbackFor = Exception.class)`来指定任何类型的异常都触发回滚。 6. **事务的边界**:事务的开始和结束...

    Spring事务管理A方法内部调用B方法的回滚问题测试代码

    在Spring框架中,事务管理是核心特性之一,用于确保数据操作的一致性和完整性。当一个方法(A方法)内部调用另一个方法(B方法)时,可能会遇到事务控制...这个示例代码对于理解和调试Spring事务管理的问题非常有帮助。

    Spring中@Transactional事务回滚(含实例

    在Spring框架中,`@Transactional`注解是用于标记事务管理的重要工具,它使得开发者能够方便地在代码中声明式地控制事务的...通过实例分析和源码阅读,我们可以更深入地理解Spring事务管理机制,提升我们的编程技能。

    spring-控制事物回滚

    我们将深入探讨Spring事务管理的原理、API使用以及在实际开发中的应用。 首先,Spring事务管理有编程式和声明式两种方式。编程式事务管理通过`PlatformTransactionManager`接口和`TransactionDefinition`接口来实现...

    SpringBoot事务使用及回滚实现代码详解

    需要注意的是,在try-catch语句中,如果catch块中对可能出现的异常进行了处理,没有再手动throw异常,Spring认为该方法成功执行,不会进行回滚。此时需要调用手动回滚方法以确保事务的一致性。 此外,在finally块中...

    Spring事务传播机制.docx

    这里是事务异常回滚的地方,这里有个注意点是回滚会先用 rollbackOn 这个方法判断,默认情况下只有 RunTimeException 及 Error 是会进行回滚的,除非在@Transactional 显式声明了 rollbackFor。 二、Spring 的事务...

    Spring异常捕获且回滚事务解决方案

    Spring 异常捕获且回滚事务解决方案 在 Spring 框架中,异常捕获和回滚事务是非常重要的概念。今天,我们将讨论如何在 Spring 中捕获异常并回滚事务。 首先,让我们了解一下 Spring 的事务机制。当我们在 Spring ...

    Spring事务流程图

    Spring事务管理是Spring框架的核心特性之一,主要用于处理应用程序中的数据一致性问题。...通过理解和使用Spring事务流程图,我们可以更好地设计和优化我们的应用程序,确保在多线程环境下的数据一致性。

    spring事务操作试验

    本文将深入探讨在"spring事务操作试验"中涉及的关键知识点,并结合提供的资源进行详细阐述。 首先,Spring事务管理的核心概念是ACID(原子性、一致性、隔离性和持久性),这是所有事务系统的基础。在Spring中,事务...

    mongoDB 4.0事务回滚的辛酸历程探究

    事务回滚是事务处理中的关键部分,确保在出现错误或异常时,数据库能够恢复到事务开始之前的状态。 在升级到MongoDB 4.0之后,首先需要注意的是确保环境的兼容性。使用Homebrew升级MongoDB到4.0.0版本,同时更新`...

    Spring事务管理Demo

    Spring事务管理的目的是确保数据的一致性和完整性,尤其是在多操作、多资源的环境中。本Demo将深入探讨Spring如何实现事务的管理。 首先,Spring提供了两种主要的事务管理方式:编程式事务管理和声明式事务管理。 ...

    spring事务案例分析.zip

    通常,我们可以配置全局事务异常处理器,例如使用@ControllerAdvice和@ExceptionHandler注解,来统一处理事务回滚和错误反馈。 7. **案例分析**:"SPRING事务管理案例分析.docx"很可能包含了具体的项目实例,详细...

    使用SpringBoot注解方式处理事务回滚实现

    使用 SpringBoot 注解方式处理事务回滚实现 在本文中,我们将介绍使用 SpringBoot 注解方式处理事务回滚实现的方法,并通过示例代码进行详细的讲解。本文对于学习 SpringBoot 的开发者或者工作中需要实现事务回滚的...

    Spring事务小demo

    3. **异常回滚规则**:默认情况下,如果在@Transactional注解的方法中抛出未检查异常(继承自RuntimeException)或者Error,Spring会自动回滚事务。对于检查异常(非RuntimeException),如果不指定回滚规则,事务...

    Spring事务优缺点及使用详解.docx

    使用Spring事务管理,主要有以下步骤: 1. 配置事务管理器:在Spring配置文件中声明数据库访问技术对应的事务管理器实现类。 2. 注解事务:通过在业务方法上添加@Transactional注解,指定事务的隔离级别、传播行为...

Global site tag (gtag.js) - Google Analytics