我们有一个使用Spring Boot,JPA和Hibernate构建的REST API。 使用API的客户端具有不可靠的网络访问权限。为避免最终用户出现太多错误,我们让客户端重试不成功的请求(例如,在发生超时后)。
由于我们无法确定服务器在再次发送时尚未处理该请求,因此我们需要使POST请求具有幂等性。也就是说,发送两次相同的POST请求不得创建两次相同的资源。
为实现这一目标,我就是这样做的:
到目前为止一切顺利。
我有多个服务器实例在同一个数据库上工作,请求是负载平衡的。因此,任何实例都可以处理请求。
使用我当前的实现,可能会出现以下情况:
在这种情况下,请求已被处理两次,这是我想要避免的。
我想到了两种可能的解决方案:
我使用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;
}
}
Filter
启动事务,提交或回滚事务?这是一个好习惯吗? @Transactional("transactionManager")
的方法创建事务。使用过滤器启动或回滚事务时会发生什么?注意:我对spring,hibernate和JPA很新,而且我对事务和过滤器背后的机制了解有限。
答案 0 :(得分:0)
请求由实例1处理并需要很长时间
考虑分两个步骤分割过程。
步骤1存储请求,步骤2处理请求。在第一个请求中,您只需将所有请求数据存储在某个位置(可以是DB或队列)。在这里你可以介绍一个状态,例如'新','进行中','准备好'。 无论如何,您可以使它们同步或异步。 因此,在第二次尝试处理相同的请求时,您可以检查它是否已存储和状态。在这里,您可以回复状态或等待状态变为“准备就绪”。 因此,在过滤器中,您只需检查请求是否已存在(先前已存储过),如果是,则只获取状态和结果(如果已准备好)以发送给响应。
您可以向RequestDTO添加自定义验证注释 - @UniqueRequest,并添加@Valid以检查DB(请参阅the example)。不需要在Filter中执行此操作,而是将逻辑移动到Controller(实际上它是验证的一部分)。 这取决于你如何在这种情况下作出回应 - 只需检查BindingResult。
答案 1 :(得分:0)
基于
<块引用>为了避免给最终用户带来太多错误,我们让客户端重试不成功的请求
您似乎完全控制了客户端代码(太棒了!)以及服务器。
但是,不清楚客户端网络的问题是不稳定(连接经常随机断开和请求中止)还是缓慢(超时),因为您已经提到了两者。所以让我们分析两者!
我首先推荐的是:
然而:
那么标准的请求-响应方案可能不合适。
在这种情况下,您可以不让客户端等待响应,而是可以立即发回确认 Request received
并通过某个 TCP 套接字发送实际响应?如果操作完成,任何后续尝试都会收到一条消息,说明 Request is being processed
或最终响应(这是您操作的幂等性会有所帮助的地方)。
如果客户端网络不稳定且容易出现频繁故障,上述建议的请求和响应分离的解决方案也应该有效!
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.
) 不满意,我相信这种情况会更糟。