思路历程:实际应用中的Spring事务

1. 前言

项目功能:在外层方法中查找指定目录中的所有文件,然后针对每个文件调用发送方法:修改数据库并发送文件。
原来的实现逻辑如下:

@Transactional(rollbackFor = { Exception.class })
public void consumer() throws Exception  {
    // 其他功能代码
    
    // 获取指定文件夹下的文件列表
    List<File> fileList = getFileList();
    for(File file : fileList){
        // 针对每个文件调用send方法
        try{
            send(file);
        }catch(Exception e){
            // 获取方法抛出的异常,以便进行必要的异常的处理
            handle();
            // 手动回滚事务
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
    }
}

public void send(File file) throws Exception{
    // 其他功能代码

    // 必要的修改数据库的操作
    updateData();

    // 发送文件,如果发送失败,则抛出异常让外层方法捕获
    boolean result = sendFile();
    if(!result){
        throws new Exception("发送文件失败");
    }
}

存在问题:由于@Transactional在外层方法上,因此外层方法consumer()和内层方法send()是同属于一个事务,并且是在所有的内层方法send()执行完毕之后,整个事务才会结束。所以在多个文件的发送情况下会出现以下情况:
假设需要发送10个文件,而前6个文件成功发送,第7个文件出现异常(不一定是发送失败)。然后内层方法send()抛出异常被外层方法的try-catch捕获,然后会调用手动回滚的事务。由于前6个文件发送仍在事务中,就会将前6个文件对应的数据库的更新回滚,但是前6个文件已经发送完毕,不会回滚,最终就会导致数据不一致的情况。
需求更新:在外层方法中需要对状态表进行更新(和send()中的更新操作不是同一个表):如果所有文件都发送成功,则更新状态表的字段值为成功;如果存在发送失败的文件,则更新状态表的字段值为异常。

2、第一阶段思考

解决存在的问题以及实现需求最关键的一个点就是,外层方法要和内层方法不在一个事务中。因此首先,send()方法上也需要加上@Transactional注解

@Transactional(rollbackFor = { Exception.class })
public void send(File file) throws Exception{}

这样,consumer()和send()实际上还是在一个事务中,因为Spring默认的事务传播行为是REQUIRED,即如果当前存在一个逻辑事务,则加入该逻辑事务,否则将新建一个逻辑事务。所以send()是加入到了consumer()的事务中,两者仍是在一个事务中。
这里可以修改事务传播行为为:REQUIRES_NEW,即如果方法已运行在一个事务中,则原有事务被挂起,新的事务被创建,直到方法结束,新事务才结束,原先的事务才会恢复执行。

@Transactional(rollbackFor = { Exception.class }, propagation = Propagation.REQUIRES_NEW)
public void send(File file) throws Exception{}

该问题解决。
由于新需求是如果捕获到send()方法的异常,则会更新状态表,因此在consumer()方法中,catch到异常就不能回滚事务了,因为如果回滚那么状态表的数据就会恢复初始值,和需求不符。并且现在consumer()和send()不是一个事务 ,consumer()也就不需要回滚。外层方法修改如下:

@Transactional(rollbackFor = { Exception.class })
public void consumer() throws Exception  {
    // 其他功能代码
    
    // 更新状态表,修改为正在进行中
    updateToSending();
    
    // 标志位,如果出现发送异常则置该值为false,然后以该值进行状态的更新
    boolean flag = true;
    // 获取指定文件夹下的文件列表
    List<File> fileList = getFileList();
    for(File file : fileList){
        // 针对每个文件调用send方法
        try{
            send(file);
        }catch(Exception e){
            // 获取方法抛出的异常,以便进行必要的异常的处理
            handle();
            flag = false;
        }
    }

    // 更新状态表为成功或失败
    if(flag){
        updateToSendSuccess();
    }else{
        updateToSendFail();
    }
}

修改完毕,测试----失败
在出现异常的时候,consumer()中状态表的修改成功,但是send()中对数据的修改没有回滚

3、第二阶段思考

上一阶段出现了发生异常后数据没有回滚的现象,说明了@Transactional在这里没有发挥出作用。经过查阅资料发现,@Transactional也是捕获异常然后进行回滚操作,如果我们自己捕获了异常并进行了处理,那么@Transactional就不会生效。这里我们在consumer()中对send()方法中的异常进行catch,因此数据没有回滚,consumer()方法继续向下执行,状态表的修改没有问题。
但是,在外层方法consumer()中,捕获异常又是必须的,因为要知晓发送是否成功就需要捕获异常。
这时候想到了原来的功能实现,虽然原send()方法没有@Transactional,但是其在consumer()的事务中,并且自己捕获并处理了异常,所以也不是consumer()上的@Transactional进行的回滚操作,而是在catch子句中使用了手动回滚事务的方法TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
于是考虑在send()方法中自行捕获异常并手动回滚。于是改造send()方法如下:

@Transactional(rollbackFor = { Exception.class }, propagation = Propagation.REQUIRES_NEW)
public void send(File file) throws Exception{
    // 其他功能代码

    try{
	    // 必要的修改数据库的操作
	    updateData();
	
	    // 发送文件,如果发送失败,则抛出异常让外层方法捕获
	    boolean result = sendFile();
	    if(!result){
	        throws new Exception("发送文件失败");
	    }
    }catch(Exception e){
        // 手动回滚事务
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        // 抛出异常以供外层方法捕获处理
        throws new Exception();
    }
}

修改完毕,测试----失败!
发生异常后,send()中的数据成功回滚,但是consumer()中的状态表的修改也回滚了。。。

4、第三阶段思考

上述情况确是诡异,因为手动回滚程序在send()中,并且已经发挥其应有的作用。而consumer()中没有回滚程序(consumer()处理了来自send()的异常,而且也没有出现别的异常使得其上的@Transactional生效)。
查看日志,确实执行了updateToSendFail();方法,说明也不是程序上出了其他的错误,就是数据发生了回滚才使得最后数据值为初始状态。而这个进行了回滚的操作也只有send()中的手动回滚方法,难道说consumer()和send()还是在一个事务中?
在consumer()和send()中都加入以下代码来查看当前事务:

// 查看当前事务
logger.debug("currentTransactionName:{}", TransactionSynchronizationManager.getCurrentTransactionName());

发现consumer()和send()的事务是同一个事务consumer(事务名称为方法的名称)。也就是第一阶段的实现并没有成功。查阅资料(链接https://blog.csdn.net/leadseczgw01/article/details/106720131):
有一种说法,在一个Service内部,事务方法之间的嵌套调用,普通方法和事务方法之间的嵌套调用,都不会开启新的事务.是因为spring采用动态代理机制来实现事务控制,而动态代理最终都是要调用原始对象的,而原始对象在去调用方法时,是不会再触发事务了!
该文章提供了一种解决方法:在调用第二个方法时,通过ApplicationContext获取代理类,使用代理类调用方法。
于是改造consumer()如下:

@Transactional(rollbackFor = { Exception.class })
public void consumer() throws Exception  {
    // 其他功能代码
    
    // 更新状态表,修改为正在进行中
    updateToSending();
    
    // 标志位,如果出现发送异常则置该值为false,然后以该值进行状态的更新
    boolean flag = true;
    // 获取指定文件夹下的文件列表
    List<File> fileList = getFileList();
    for(File file : fileList){
        // 针对每个文件调用send方法
        try{
            // 获取bean
            ConsumerServiceImpl bean = applicationContext.getBean( ConsumerServiceImpl.class);
            bean.send(file);
        }catch(Exception e){
            // 获取方法抛出的异常,以便进行必要的异常的处理
            handle();
            flag = false;
        }
    }

    // 更新状态表为成功或失败
    if(flag){
        updateToSendSuccess();
    }else{
        updateToSendFail();
    }
}

其他问题:由于ConsumerServiceImpl是ConsumerService的其中一个实现类,因此直接通过class来获取bean会出现NoUniqueBeanDefinitionException异常,因此需要指定名称:

ConsumerServiceImpl bean = applicationContext.getBean("consumerServiceImpl", ConsumerServiceImpl.class);

修改完毕,测试----终于成功!!!
send()出现异常后,对应本次的更新操作全部回滚,但是在此之前成功的数据正常更新,consumer()中状态表修改也没有问题。

注:本篇文章为解决问题时的一篇思路历程,由于对这部分知识点不甚精通,因此不代表问题的最优解,如有错误或更好的方法欢迎指正。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值