REST幂等性实现-已处理请求后如何回滚?

Mul*_*ure 5 rest spring hibernate transactions idempotent

我正在努力实现的目标

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

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

到目前为止我做了什么

为此,我做了以下工作:

  • 客户端正在使用自定义HTTP标头发送UUID和请求。
  • 当客户端重新发送相同的请求时,将发送相同的UUID。
  • 服务器第一次处理请求时,对请求的响应与UUID一起存储在数据库中。
  • 第二次接收到相同的请求时,将从数据库中检索结果,并做出响应而无需再次处理该请求。

到目前为止,一切都很好。

问题

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

在我当前的实现中,可能会发生以下情况:

  1. 该请求由实例1处理,需要很长时间
  2. 由于花费的时间太长,客户端将中止连接并重新发送相同的请求
  3. 第二个请求由实例2处理
  4. 第一个请求处理完成,结果按实例1保存在数据库中
  5. 第二个请求处理完成。当实例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);
        }

    }

    ...
Run Code Online (Sandbox Code Playgroud)

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

@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; 
    }

}
Run Code Online (Sandbox Code Playgroud)

问题

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

注意:我对spring,hibernate和JPA还是比较陌生,并且对事务和过滤器背后的机制了解有限。

Avi*_*ius 0

Based on

To avoid having too many errors for the end user, we made the client retry unsuccessful requests

you seem to have full control of the client code (great!) as well as the server.

It is, however, not clear whether the problem with the client's network is flakiness (the connection often randomly drops and requests are aborted) or slowness (timeouts), since you've mentioned both. So let's analyse both!

Timeouts

The first things that I'd recommend are:

  1. adjusting the connection timeout on the server so that it is not closed before the server finishes the operation;
  2. adjusting the request timeout on the client to account for the slow operation on the server and the slowness of the client's network.

However:

  • if the server operation is really slow and the maximum connection timeout (120s, is it?) is not enough;
  • or if you are also sending large requests/responses and the maximum client timeout is not enough;
  • or if you just don't want to increase the timeouts for any reason,

then the standard request-response scheme is probably not suitable.

In this case, instead of having the client wait for a response you could perhaps send back an immediate acknowledgement Request received and send the actual response via some TCP socket? Any following attempts would receive either a message saying that the Request is being processed, or the final response, if the operation is complete (this is where the idempotence of your operation would help).

Client network failures

If the client network is flaky and prone to frequent failures, the above proposed solution, where requests and responses are uncoupled, should work too!

  1. First of all, if you send back immediate acknowledgements, you'd let the client know what's going on immediately; a quick response time should also make it more likely that the client receives the response.
  2. 其次,每当任何请求由于网络故障而中止时,您只需等待适当的时间(基本上,有足够的时间让服务器完成操作)然后再尝试,而不是立即重试。这样,您将显着增加服务器完成相关操作并且您应该得到响应的机会(同样,这是使用幂等请求至关重要的地方)。
  3. 如果您不想调整超时,或者在重试该操作后收到一条响应Request in progress,您可以尝试再次监听套接字。

最后的想法

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

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

如果您不拥有客户端代码

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