`
Tyrion
  • 浏览: 257615 次
  • 性别: 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事务管理只对出现运行期异常进行回滚

    Spring的事务管理默认只对出现运行期异常(java.lang.RuntimeException及其子类)进行回滚,需要了解更多Spring事务方面的知识,可详看本

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

    主要介绍了Spring异常捕获且回滚事务解决方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下

    spring五种事务配置demo

    测试spring事务管理 搭建了ssh框架的web工程 本工程用到的数据库表很简单 user(id, name) 可自行创建 本例所有的事务放在service层进行管理,方法中间抛出运行时异常以测试是否回滚 Spring配置文件中关于事务...

    Spring2.5和Hibernate3集成--学习spring aop ioc

    * 默认事务回滚异常是RuntiimeException(包括所有继承RuntimeException的子类).普通异常不回滚 * 在编写业务逻辑方法时,最好将异常一直往上抛出,由表示层处理(Struts) * spring的事务管理需要添加到业务逻辑上...

    springboot实现多数据源而且加上事务不会使aop切换数据源失效

    用springboot 集成mybatis多数据源,用aop实现的动态切换,支持事务,不会使aop动态切换失效。注意:此代码不涉及分布式事务,如果需要分布式事务 需要采取其他方案。

    基于springcloud+springboot+nacos+openFeign的分布式事务组件seata项目源码.zip

    分布式事务组件seata的使用demo,AT模式、TCC模式,集成springboot、springcloud(nacos注册中心、openFeign服务调用、Ribbon负载均衡器)、spring jpa,数据库采用mysql demo中使用的相关版本号,具体请看代码。...

    hmily-master.zip

    高可靠性 :支持分布式场景下,事务异常回滚,超时异常恢复,防止事务悬挂 易用性 :提供零侵入性式的 Spring-Boot, Spring-Namespace 快速与业务系统集成 高性能 :去中心化设计,与业务系统完全融合,天然支持集群...

    尚硅谷佟刚Spring4代码及PPT.rar

    JdbcDaoSupport、使用 NamedParameterJdbcTemplate、Spring 的声明式事务、事务的属性(传播行为、隔离级别、回滚属性、只读属性、过期时间)、使用 XML 文件的方式配置事务、整合 Hibernate、整合 Struts2 等。

    Spring-Reference_zh_CN(Spring中文参考手册)

    在应用服务器中使用Hibernate的注意点 12.3. JDO 12.3.1. 建立PersistenceManagerFactory 12.3.2. JdoTemplate和JdoDaoSupport 12.3.3. 基于原生的JDO API实现DAO 12.3.4. 事务管理 12.3.5. JdoDialect 12.4. ...

    Spring 2.0 开发参考手册

    在应用服务器中使用Hibernate的注意点 12.3. JDO 12.3.1. 建立PersistenceManagerFactory 12.3.2. JdoTemplate和JdoDaoSupport 12.3.3. 基于原生的JDO API实现DAO 12.3.4. 事务管理 12.3.5. JdoDialect 12.4...

    Spring笔记说明文件

    3)、ProxyTransactionManagementConfiguration 做了什么? 1、给容器中注册事务增强器; 1)、事务增强器要用事务注解... 如果异常,获取到事务管理器,利用事务管理回滚操作; 如果正常,利用事务管理器,提交事务

    Spring中文帮助文档

    在应用服务器中使用Hibernate的注意事项 12.3. JDO 12.3.1. 建立PersistenceManagerFactory 12.3.2. JdoTemplate和JdoDaoSupport 12.3.3. 基于原生的JDO API实现DAO 12.3.4. 事务管理 12.3.5. JdoDialect ...

    Spring API

    在应用服务器中使用Hibernate的注意事项 12.3. JDO 12.3.1. 建立PersistenceManagerFactory 12.3.2. JdoTemplate和JdoDaoSupport 12.3.3. 基于原生的JDO API实现DAO 12.3.4. 事务管理 12.3.5. JdoDialect ...

    spring chm文档

    在应用服务器中使用Hibernate的注意点 12.3. JDO 12.3.1. 建立PersistenceManagerFactory 12.3.2. JdoTemplate和JdoDaoSupport 12.3.3. 基于原生的JDO API实现DAO 12.3.4. 事务管理 12.3.5. JdoDialect 12.4...

    Spring.html

    概念:面向切面编程,在不改变源码的情况下对方法进行增强,抽取横切关注点(日志处理,事务管理,安全检查,性能测试等等),使用AOP进行增强,使程序员只需要关注与业务逻辑编写. 专业术语 目标Target:需要增强的类 ...

    lcn-demo.rar

    基于LCN 的微服务事务管理项目Demo。管理SpringCloud + springboot微服务之间的事务 处理,异常回滚操作。

    第24次课-1 Spring与Hibernate的整合

    如遇异常,回滚事务 关闭Session 24.3 Spring对Hibernate的简化 24.3.1 概述 Spring提供的持久层访问的方式,无须显式地打开和关闭Session,也无须在代码中执行任何的事务操作语句。 Spring提供了HibernateTemplate...

    jta分布式事务完成例子,测试通过

    先向tb1插一条数据,然后在向tb2插一条数据,当没有设置事务时,如果tb2出现异常,tb1能正常插入数据,当设置了分布式事务后,如果tb2出现异常,tb1会自动回滚,没有数据插入。 分布式事务是针对不同数据库的(当然...

    【Spring源码解析】事务流程解析

    抛出异常,事务回滚。最小的执行单位为方法。决定执行成败是通过是否抛出异常来判断的,抛出异常即执行失败。 2、代码 (1) mysql建表语句 CREATE TABLE `t_student` ( `c_id` int(11) NOT NULL AUTO_INCREMENT, `c...

Global site tag (gtag.js) - Google Analytics