【问题标题】:Best practice to 'rollback' REST method calls inside method在方法内“回滚”REST 方法调用的最佳实践
【发布时间】:2018-08-19 22:01:20
【问题描述】:

标题可能不正确,但我会尽力解释我的问题。我的项目是一个 Spring Boot 项目。我有调用外部 REST 端点的服务。

我有一个服务方法,其中包含对我拥有的其他服务的多个方法调用。每个单独的方法调用都可能成功或失败。每个方法调用都是针对 REST 端点进行的,并且可能会出现诸如 Web 服务不可用或在极少数情况下引发未知异常等问题。无论发生什么,我需要能够跟踪哪些方法调用是成功的,如果其中任何一个调用失败,我想回滚到原始状态,就好像什么都没发生一样,将其视为@Transactional 注释。所有 REST 调用都是不同的端点,需要单独调用,并且来自我没有影响的外部方。示例:

public MyServiceImpl implements MyService {
@Autowired
private Process1Service;
@Autowired
private Process2Service;
@Autowired
private Process3Service;
@Autowired
private Process4Service;

public void bundledProcess() {
       process1Service.createFileRESTcall();
       process2Service.addFilePermissionsRESTcall();
       process3Service.addFileMetadataRESTcall(); <-- might fail for example
       process4Service.addFileTimestampRESTcall();       
  }
}

如果例如 process3Service.addFileMetadataRESTcall 失败,我想为 process3 之前的每个步骤执行类似撤消(以相反顺序)的操作:

process2Service.removeFilePermissionsRESTcall();
process1Service.deleteFileRESTcall();

我阅读了有关命令模式的信息,但它似乎用于应用程序内的撤消操作,作为执行操作的一种历史记录,而不是在 Spring Web 应用程序内。这对我的用例是否也正确,或者如果成功,我应该跟踪每个方法/网络服务调用吗?是否有执行此操作的最佳做​​法?

我想无论我如何跟踪它,我都需要知道哪个方法调用失败并从那里执行我的“撤消”方法 REST 调用。虽然理论上即使这些调用当然也可能失败。

我的主要目标是不创建未执行任何进一步处理的文件(在我的示例中)。它应该要么全部成功,要么一无所获。一种事务性的。

Update1:​​基于 cmets 改进的伪实现:

public Process1ServiceImpl implements Process1Service {
    public void createFileRESTcall() throws MyException {
         // Call an external REST api, pseudo code:
         if (REST-call fails) {
            throw new MyException("External REST api failed");
         }                         
    }
}

public class BundledProcessEvent {
    private boolean createFileSuccess;
    private boolean addFilePermissionsSuccess;
    private boolean addFileMetadataSuccess;
    private boolean addFileTimestampSuccess;

    // Getters and setters
}

public MyServiceImpl implements MyService {
@Autowired
private Process1Service;
@Autowired
private Process2Service;
@Autowired
private Process3Service;
@Autowired
private Process4Service;
@Autowired
private ApplicationEventPublisher applicationEventPublisher;

@Transactional(rollbackOn = MyException.class)
public void bundledProcess() {
   BundleProcessEvent bundleProcessEvent = new BundleProcessEvent();
   this.applicationEventPublisher.publishEvent(bundleProcessEvent);

   bundleProcessEvent.setCreateFileSuccess = bundprocess1Service.createFileRESTcall();
   bundleProcessEvent.setAddFilePermissionsSuccess = process2Service.addFilePermissionsRESTcall();
   bundleProcessEvent.setAddFileMetadataSuccess = process3Service.addFileMetadataRESTcall();
   bundleProcessEvent.setAddFileTimestampSuccess = process4Service.addFileTimestampRESTcall();
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
public void rollback(BundleProcessEvent bundleProcessEvent) {
      // If the last process event is successful, we should not
      // be in this rollback method even
      //if (bundleProcessEvent.isAddFileTimestampSuccess()) {
         // remove timestamp
      //}

      if (bundleProcessEvent.isAddFileMetadataSuccess()) {
         // remove metadata
      }

      if (bundleProcessEvent.isAddFilePermissionsSuccess()) {
         // remove file permissions
      }

      if (bundleProcessEvent.isCreateFileSuccess()) {
         // remove file
      }
}

【问题讨论】:

  • 除了以下答案之外,如果可能并且下游系统允许,您可以查看补偿交易。这基本上是在进行与原始交易完全相反的另一笔交易。

标签: java rest spring-boot rollback undo


【解决方案1】:

这个问题的答案相当广泛。有多种方法可以进行分布式事务以在此处完成所有操作。但是,由于您使用的是 Java 和 Spring,因此最好的选择是使用 JTA(Java Transaction API)之类的东西,它可以跨多个服务/实例/等进行分布式事务。幸运的是,Spring Boot 使用 Atomikos 或 Bitronix 支持 JTA .你可以阅读文档here

启用分布式事务的一种方法是通过 JMS、RabbitMQ、Kafka、ActiveMQ 等消息代理并使用 XA 事务(两阶段提交)等协议。对于不支持分布式的外部服务,一种方法是编写一个包装服务,该服务可以理解该外部服务的 XA 事务。

【讨论】:

    【解决方案2】:

    你的操作看起来像一个事务,所以你可以使用@Transactional注解。从您的代码中,我无法真正告诉您如何管理每个操作的 HTTP 响应调用,但您应该考虑让您的服务方法返回它们,然后根据响应调用进行回滚。你可以像这样创建一个方法数组,但你希望你的逻辑究竟是怎样的,取决于你。

    private Process[] restCalls = new Process[] {
            new Process() { public void call() { process1Service.createFileRESTcall(); } },
            new Process() { public void call() { process2Service.addFilePermissionsRESTcall(); } },
            new Process() { public void call() { process3Service.addFileMetadataRESTcall(); } },
            new Process() { public void call() { process4Service.addFileTimestampRESTcall(); } },
    };
    
    interface Process {
        void call();
    }
    
    @Transactional(rollbackOn = Exception.class)
    public void bundledProcess() {
        restCalls[0].call(); 
        ... // say, see which process returned wrong response code
    }
    
    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
    public void rollback() {
        // handle rollback according to failed method index
    }
    

    检查this article.可能会派上用场。

    【讨论】:

    • 我认为@Transactional 不是解决这个问题的正确方法
    • 我用基于使用@Transactional 的示例实现更新了我的原始帖子,它现在对我有用。虽然我仍然愿意接受改进的解决方案或不同的方法。
    猜你喜欢
    • 2020-01-25
    • 1970-01-01
    • 2017-01-20
    • 1970-01-01
    • 1970-01-01
    • 2019-11-08
    • 1970-01-01
    • 2012-10-09
    • 1970-01-01
    相关资源
    最近更新 更多