实现幂等键

Implementing idempotency keys

我正在尝试让我的两个 Golang GRPC 端点支持幂等键。我的服务将从 Mongo 中存储和读取键(因为我已经将它用于其他数据)作为它自己的集合中的唯一索引。

我正在考虑两种解决方案,但各有其弱点。我知道还有更复杂的东西,比如保存请求和响应以及制作逻辑 ACID。但是,对于我的第一个端点,only-once logic(需要幂等的端点代码)调用发送电子邮件的服务,因此无法回滚。我的第二个端点在 Mongo 中执行了多个插入,这似乎可以回滚,但我不确定如何以及是否有另一个解决方案也可以解决第一个端点。

解决方案 1

func MyEndpoint(request Request) (Response, error) {
  doesExist, err := doesIdemKeyExist(request.IdemKey)
  if err != nil {
    return nil, status.Error(codes.Internal, "Failed to check idem key.")
  }
  if doesExist {
    return Response{}, nil
  }
  
  // < only-once logic >

  err := insertIdemKey(request.IdemKey)
  if err != nil {
    if mongo.IsDuplicateKeyError(err) {
      return Response{}, nil
    }
    return nil, status.Error(codes.Internal, "Failed to insert idem key.")
  }
 
  return Response{}, nil
}

这里的弱点是客户端可以向我的端点发送第一个请求并失去连接,然后重试第二个请求。第一个请求可以处理但无法到达 insertIdemKey,因此第二个请求也会处理,这违反了幂等性。

解决方案 2

func MyEndpoint(request Request) (Response, error) {
  err := insertIdemKey(request.IdemKey)
  if err != nil {
    if mongo.IsDuplicateKeyError(err) {
      return Response{}, nil
    }
    return nil, status.Error(codes.Internal, "Failed to insert idem key.")
  }

  // < only-once logic >

  return Response{}, nil
}

这里的弱点是 only-once logic 可能会出现间歇性故障,例如依赖项。重试的受影响请求将被忽略。

最好的解决方案是什么?我应该妥协并采用这些不完美的解决方案之一吗?

您应该在 MongoDB 中使用 状态 属性 的文档,可能的值为 processingdone

当收到请求时,尝试使用给定的 idemKeystate=processing 将文档插入数据库。如果因为密钥已经存在而失败,则报告成功(如果状态为 done)或者它仍在处理中(如果状态为 processing)。或者等待它完成然后报告成功。

如果插入文档成功,继续执行“only-once逻辑”。

完成“only-once 逻辑”后,将文档的状态更新为 state=done。如果执行逻辑失败,您可以从数据库中删除该文档,以便后续请求可以尝试再次执行它。

为了防止在执行逻辑期间发生服务器故障或防止删除文档失败,您还应该记录开始/创建时间戳,并定义到期时间。比方说,当一个新请求进来并且文档存在 processing 状态但文档早于 30 秒时,您可以假设它永远不会完成并继续处理,就好像该文档在数据库中不存在一样第一处:将其创建时间戳设置为当前时间并执行逻辑,如果逻辑执行成功则将状态更新为done。 MongoDB 也支持 , but note that the removal is .

请注意,此解决方案也不完美:如果执行逻辑成功但之后您无法将文档的状态更新为 done,则到期后您可能最终会重复执行。你想要的是你的逻辑一个MongoDB操作的原子/事务执行,这是不可能的。

如果您的“only-once 逻辑”包含多个插入,您可以使用 insertOrUpdate() 在执行失败时不复制记录并且您必须重复它,或者您可以插入文档idemKey 包括在内,这样您就可以确定之前插入了哪些文档(您可以先删除它们,或者跳过它们,只插入其余的)。
另请注意,从 MongoDB 5.0 开始,transactions are supported,因此您可以在单个事务中执行多个插入。

参见相关问题: