实施幂等键

huangapple go评论90阅读模式
英文:

Implementing idempotency keys

问题

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

我考虑了两种解决方案,但每种方案都有其弱点。我知道还有更复杂的方法,比如保存请求和响应以及使逻辑具有原子性。然而,对于我的第一个端点,只执行一次逻辑(需要具有幂等性的端点代码)调用一个发送电子邮件的服务,因此无法回滚。我的第二个端点在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
  }
  
  // <只执行一次逻辑>
  
  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.")
  }
  
  // <只执行一次逻辑>
  
  return Response{}, nil
}

这种方法的弱点是只执行一次逻辑可能会出现间歇性故障,例如来自依赖项的故障。重新尝试的受影响请求将被忽略。

在这里,什么是最好的解决方案?我应该妥协并选择其中一个不完美的解决方案吗?

英文:

I'm trying to get my two Golang GRPC endpoints to support idempotency keys. My service will store and read keys from Mongo (because I'm already using it for other data) as a unique index in its own Collection.

I'm thinking of two solutions but each has their weaknesses. I know there's more complex stuff like saving request and response and making the logic ACID. However for my first endpoint, the only-once logic (the endpoint's code which needs to be idempotent) calls a service that sends an email, so it can't be rollbacked. My second endpoint does multiple Inserts in Mongo, which seems can be rollbacked but I'm not sure how and if there's another solution that'd also solve for the first endpoint.

Solution 1

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

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

The weakness here is that client could send first request to my endpoint and lose connection, then retry with second request. First request could process but not reach insertIdemKey, so second request would process too, violating idempotency.

Solution 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, &quot;Failed to insert idem key.&quot;)
  }

  // &lt; only-once logic &gt;

  return Response{}, nil
}

The weakness here is that only-once logic could have intermittent failures, such as from dependencies. Affected requests that are retried will be ignored.

What's the best solution here? Should I just compromise and go with one of these imperfect solutions?

答案1

得分: 1

你应该在MongoDB中使用一个带有_state_属性的文档,可能的取值为processingdone

当有一个请求到达时,尝试将带有给定idemKeystate=processing的文档插入到数据库中。如果由于键已经存在而插入失败,则报告成功(如果状态为done)或者仍在处理中(如果状态为processing)。或者等待它完成,然后报告成功。

如果插入文档成功,则继续执行“只执行一次逻辑”。

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

为了防止在执行逻辑期间服务器故障或无法删除文档的故障,您还应记录开始/创建时间戳,并定义一个过期时间。假设当一个新的请求到达时,文档存在于processing状态,但文档的年龄超过30秒,您可以假设它永远不会完成,并按照文档在数据库中不存在的情况继续进行:将其创建时间戳设置为当前时间并执行逻辑,然后如果逻辑执行成功,则将状态更新为done。MongoDB还支持自动删除过期文档,但请注意,删除操作不是精确定时的

请注意,这个解决方案也不是完美的:如果执行逻辑成功,但无法在之后将文档的状态更新为done,在过期后,您可能会重复执行。您希望的是逻辑和MongoDB操作的原子/事务执行,这是不可能的。

如果您的“只执行一次逻辑”包含多个插入操作,您可以使用insertOrUpdate()来避免在执行失败并需要重复执行时重复记录,或者您可以在插入文档时包含idemKey,这样您可以识别先前插入的文档(您可以首先删除它们,或者跳过它们,只插入其余部分)。
还要注意,从MongoDB 5.0开始,支持事务,因此您可以在单个事务中执行多个插入操作。

参考相关问题:https://stackoverflow.com/questions/50488013/how-to-synchronize-two-apps-running-in-two-different-servers-via-mongodb/50488423#50488423

英文:

You should use a document with a state property in MongoDB, with possible values processing and done.

When a request comes in, try to insert the document into the database with the given idemKey and state=processing. If that fails because the key already exists, then either report success (if state is done) or that it is still being processed (if state is processing). Or wait for it to complete and then report success.

If inserting the document succeeds, proceed with executing "only-once logic".

Once the "only-once logic" is done, update the document's state to state=done. If executing the logic fails, you may delete the document from the database so a subsequent request can try to execute it again.

To protect against a server failure during executing the logic or against a failure to delete the document, you should record the start / creation timestamp too, and define an expiration. Let's say when a new request comes in and the document exists with processing sate but the document is older than 30 seconds, you could assume it will never be completed and proceed as if the document didn't exist in the database in the first place: set its creation timestamp to the current time and execute the logic, then update the state to done if logic execution succeeds. MongoDB also supports auto-removal of expired documents, but note that the removal is not timed precisely.

Note that this solution isn't perfect either: if executing the logic succeeds but you can't update the document's state to done afterwards, after expiration you could end up repeating the execution. What you want is the atomic / transactional execution of your logic and a MongoDB operation, which is not possible.

If your "only-once logic" contains multiple inserts, you could use insertOrUpdate() to not duplicate the records if the execution fails and you have to repeat it, or you could insert the documents with idemKey included, so you could identify which documents were previously inserted (and you could remove them first thing, or skip them and just insert the rest).
Also note that starting with MongoDB 5.0, transactions are supported, so you can execute multiple inserts in a single transaction.

See related question: https://stackoverflow.com/questions/50488013/how-to-synchronize-two-apps-running-in-two-different-servers-via-mongodb/50488423#50488423

huangapple
  • 本文由 发表于 2022年2月25日 15:56:59
  • 转载请务必保留本文链接:https://go.coder-hub.com/71262703.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定