实现幂等键
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 中使用 状态 属性 的文档,可能的值为 processing
和 done
。
当收到请求时,尝试使用给定的 idemKey
和 state=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,因此您可以在单个事务中执行多个插入。
参见相关问题:
我正在尝试让我的两个 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 中使用 状态 属性 的文档,可能的值为 processing
和 done
。
当收到请求时,尝试使用给定的 idemKey
和 state=processing
将文档插入数据库。如果因为密钥已经存在而失败,则报告成功(如果状态为 done
)或者它仍在处理中(如果状态为 processing
)。或者等待它完成然后报告成功。
如果插入文档成功,继续执行“only-once逻辑”。
完成“only-once 逻辑”后,将文档的状态更新为 state=done
。如果执行逻辑失败,您可以从数据库中删除该文档,以便后续请求可以尝试再次执行它。
为了防止在执行逻辑期间发生服务器故障或防止删除文档失败,您还应该记录开始/创建时间戳,并定义到期时间。比方说,当一个新请求进来并且文档存在 processing
状态但文档早于 30 秒时,您可以假设它永远不会完成并继续处理,就好像该文档在数据库中不存在一样第一处:将其创建时间戳设置为当前时间并执行逻辑,如果逻辑执行成功则将状态更新为done
。 MongoDB 也支持
请注意,此解决方案也不完美:如果执行逻辑成功但之后您无法将文档的状态更新为 done
,则到期后您可能最终会重复执行。你想要的是你的逻辑和一个MongoDB操作的原子/事务执行,这是不可能的。
如果您的“only-once 逻辑”包含多个插入,您可以使用 insertOrUpdate()
在执行失败时不复制记录并且您必须重复它,或者您可以插入文档idemKey
包括在内,这样您就可以确定之前插入了哪些文档(您可以先删除它们,或者跳过它们,只插入其余的)。
另请注意,从 MongoDB 5.0 开始,transactions are supported,因此您可以在单个事务中执行多个插入。
参见相关问题: