REST Idempotence实现 - 如何在处理请求时回滚?

时间:2017-07-26 13:14:52

标签: spring hibernate rest transactions idempotent

我想要实现的目标

我们有一个使用Spring Boot,JPA和Hibernate构建的REST API。 使用API​​的客户端具有不可靠的网络访问权限。为避免最终用户出现太多错误,我们让客户端重试不成功的请求(例如,在发生超时后)。

由于我们无法确定服务器在再次发送时尚未处理该请求,因此我们需要使POST请求具有幂等性。也就是说,发送两次相同的POST请求不得创建两次相同的资源。

到目前为止我做了什么

为实现这一目标,我就是这样做的:

  • 客户端在自定义HTTP标头中发送UUID以及请求。
  • 当客户端重新发送相同的请求时,会发送相同的UUID。
  • 服务器第一次处理请求时,请求的响应与UUID一起存储在数据库中。
  • 第二次收到相同的请求时,将从数据库中检索结果,并在不再处理请求的情况下进行响应。

到目前为止一切顺利。

问题

我有多个服务器实例在同一个数据库上工作,请求是负载平衡的。因此,任何实例都可以处理请求。

使用我当前的实现,可能会出现以下情况:

  1. 请求由实例1处理并需要很长时间
  2. 因为它需要太长时间,客户端会中止连接并重新发送相同的请求
  3. 第二个请求由实例2处理
  4. 第一个请求处理完成,结果由实例1
  5. 保存在数据库中
  6. 第2次请求处理结束。当实例2尝试将结果存储在数据库中时,结果已存在于数据库中。
  7. 在这种情况下,请求已被处理两次,这是我想要避免的。

    我想到了两种可能的解决方案:

    1. 在已存储相同请求的结果时回滚请求2,并将保存的响应发送到客户端。
    2. 一旦实例1开始处理请求ID,就通过在数据库中保存请求ID来阻止处理请求2。当超时关闭客户端和实例1之间的连接时,此解决方案将无法工作,从而使客户端无法实际接收实例1处理的响应。
    3. 尝试解决方案1 ​​

      我使用Filter来检索和存储回复。我的过滤器看起来大致如下:

      @Component
      public class IdempotentRequestFilter implements Filter {
      
          @Override
          public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException     {
      
              String requestId = getRequestId(request);
      
      
              if(requestId != null) { 
      
                  ResponseCache existingResponse = getExistingResponse(requestId);
      
                  if(existingResponse != null) {
                      serveExistingResponse(response, existingResponse);
                  }
                  else {
      
                      filterChain.doFilter(request, response);
      
                      try {
                          saveResponse(requestId, response);
                          serve(response);
                      }
                      catch (DataIntegrityViolationException e) {
      
                          // Here perform rollback somehow
      
                          existingResponse = getExistingResponse(requestId);
                          serveExistingResponse(response, existingResponse);
                      }
                  }
              }
              else {
                  filterChain.doFilter(request, response);
              }
      
          }
      
          ...
      

      我的请求将按照以下方式处理:

      @Controller 
      public class UserController {
      
          @Autowired 
          UserManager userManager; 
      
          @RequestMapping(value = "/user", method = RequestMethod.POST)
          @ResponseBody
          public User createUser(@RequestBody User newUser)  {
              return userManager.create(newUser);
          }
      }
      
      @Component
      @Lazy
      public class UserManager {
      
          @Transactional("transactionManager")
          public User create(User user) {
              userRepository.save(user); 
              return user; 
          }
      
      }
      

      问题

      • 你能想到任何其他解决方案来避免这个问题吗?
      • 是否有任何其他解决方案使POST请求具有幂等性(或许完全不同)?
      • 如何从上面显示的Filter启动事务,提交或回滚事务?这是一个好习惯吗?
      • 处理请求时,现有代码已通过调用多个注释为@Transactional("transactionManager")的方法创建事务。使用过滤器启动或回滚事务时会发生什么?

      注意:我对spring,hibernate和JPA很新,而且我对事务和过滤器背后的机制了解有限。

2 个答案:

答案 0 :(得分:0)

  

请求由实例1处理并需要很长时间

考虑分两个步骤分割过程。

步骤1存储请求,步骤2处理请求。在第一个请求中,您只需将所有请求数据存储在某个位置(可以是DB或队列)。在这里你可以介绍一个状态,例如'新','进行中','准备好'。 无论如何,您可以使它们同步或异步。 因此,在第二次尝试处理相同的请求时,您可以检查它是否已存储和状态。在这里,您可以回复状态或等待状态变为“准备就绪”。 因此,在过滤器中,您只需检查请求是否已存在(先前已存储过),如果是,则只获取状态和结果(如果已准备好)以发送给响应。

您可以向RequestDTO添加自定义验证注释 - @UniqueRequest,并添加@Valid以检查DB(请参阅the example)。不需要在Filter中执行此操作,而是将逻辑移动到Controller(实际上它是验证的一部分)。 这取决于你如何在这种情况下作出回应 - 只需检查BindingResult。

答案 1 :(得分:0)

基于

<块引用>

为了避免给最终用户带来太多错误,我们让客户端重试不成功的请求

您似乎完全控制了客户端代码(太棒了!)以及服务器。

但是,不清楚客户端网络的问题是不稳定(连接经常随机断开和请求中止)还是缓慢(超时),因为您已经提到了两者。所以让我们分析两者!

超时

我首先推荐的是:

  1. 调整服务器上的连接超时时间,使其在服务器完成操作前不被关闭;
  2. 调整客户端上的请求超时以应对服务器上的缓慢操作客户端网络的缓慢。

然而:

  • 如果服务器操作真的很慢并且最大连接超时(120 秒,是吗?)不够;
  • 或者如果您还发送大量请求/响应并且最大客户端超时时间不够;
  • 或者如果您出于任何原因不想增加超时时间,

那么标准的请求-响应方案可能不合适。

在这种情况下,您可以不让客户端等待响应,而是可以立即发回确认 Request received 并通过某个 TCP 套接字发送实际响应?如果操作完成,任何后续尝试都会收到一条消息,说明 Request is being processed 或最终响应(这是您操作的幂等性会有所帮助的地方)。

客户端网络故障

如果客户端网络不稳定且容易出现频繁故障,上述建议的请求和响应分离的解决方案也应该有效!

  1. 首先,如果您立即发回确认,您会立即让客户知道发生了什么;快速的响应时间还应该使客户更有可能收到响应。
  2. 其次,每当任何请求因网络故障而中止时,您只需等待适当的时间(基本上是服务器完成操作的足够时间),然后再试一次,而不是立即再试一次。通过这种方式,您可以显着增加服务器完成相关操作的机会,并且您应该得到响应(同样,这也是使用幂等请求至关重要的地方)。
  3. 如果您不希望调整超时时间,以防在重试操作后您收到说 Request in progress 的响应,您可以尝试再次侦听套接字。

最后的想法

如果不能选择使用套接字,则可以使用轮询。轮询不是很好,但就我个人而言,我很可能仍然使用轮询而不是回滚,尤其是在服务器操作缓慢的情况下 - 这将允许在重试之前进行适当的暂停。

回滚的问题在于它们会尝试使用代码从故障中恢复,而代码本身从来都不是万无一失的。如果回滚时出现问题怎么办?你能确保回滚是原子的和幂等的,并且在任何情况下都不会让系统处于未定义状态吗?除此之外,它们可能难以实施,并且会引入额外的复杂性和额外的测试和维护代码。

如果您不拥有客户端代码

如果您不拥有客户端代码,您会遇到更多麻烦,因为您的 API 的使用者可以随意对您的服务器进行大量任意调用。在这种情况下,我会绝对锁定幂等操作并返回响应,表示正在处理请求,而不是尝试使用回滚来还原任何内容。想象一下有多个并发请求和回滚!如果您对 Stanislav's proposal (The queue will get longer and longer, making the whole system slower, reducing the capacity of the system to serve requests.) 不满意,我相信这种情况会更糟。