2017-07-26 49 views
2

我正在尝试实现的内容

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

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

我做了什么至今

要做到这一点,这里是我做过什么:

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

到目前为止好。

问题

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

从我目前的执行情况,可能会出现以下情况:

  1. 请求由实例1处理,需要很长的时间
  2. 因为时间太长,客户端终止连接并重新发送相同的请求
  3. 该第二请求是由实例2
  4. 的第一请求处理结束处理,并且结果是通过实例1
  5. 的第二REQ保存在数据库加工完成。当实例2尝试将结果存储在数据库中时,结果已存在于数据库中。

在这种情况下,该请求已处理两次,这是我想要避免的。

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

  1. 回滚时,对同一请求的结果已经存储的请求2,发送到客户端的响应保存。
  2. 只要实例1开始处理它,就通过将请求ID保存在数据库中来防止请求2被处理。这种解决方案将不作为客户端和实例1之间的连接工作是由超时关闭,使得不能为客户端实际接收由实例1

尝试对溶液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相当陌生,而且我对交易和过滤器背后的机制的理解有限。

回答

0

该请求由实例1处理,并且需要很长的时间

考虑分裂过程中2个步骤。

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

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

+0

您对存储与地位的要求的解决方案有一个缺点:在每个请求的尝试,你会得到一个新的进程“等待”另一到结束。队列将变得越来越长,使得整个系统变慢,从而降低了系统为请求提供服务的能力。 [点击这里阅读](https://stackoverflow.com/questions/31621970/how-to-keep-an-api-idempotent-while-receiving-multiple-requests-with-the-same-id) 关于验证注解,它看起来像我必须为每个请求编写特定的回滚代码。代码基数很大,我宁愿避免它。我会尽量探索它。 – Laure