使 POST 请求幂等

Making POST requests idempotent

我一直在寻找一种方法来设计我的 API 所以它是幂等的,这意味着其中一些是为了让我的 POST 请求路由是幂等的,我偶然发现 this条。

(如果我理解的不对,请指正!)

里面,对大体思路有很好的解释。但缺少的是他自己实现的方式的一些例子。

有人问文章的作者,他是如何保证原子性的?所以作者添加了一个代码示例。

本质上,在his code example中有两种情况,

如果一切顺利的话流程:

代码内部出错时的流程:

注意,打开的事务是针对某个DB的,暂且称他为A。 但是与他同样使用的redis store无关,也就是说事务的回滚只会影响DB A。

所以它涵盖了代码内部发生某些事情导致无法完成交易的情况。

但是如果代码 运行 所在的机器崩溃了,当它处于已经执行了 Set expire time to that key 并且现在即将执行的状态时会发生什么运行交易提交?

在这种情况下,密钥将在 redis 存储中可用,但事务尚未提交。 这将导致服务确定所需的更改已经发生,但实际上并没有发生,机器在完成之前就失败了。

我需要这样设计API,如果在redis中更改数据或设置键和值失败,它们都会回滚。

这个问题的解决方案是什么?

如何保证在一个数据库中更改所需数据的原子性,同时在redis中设置键和所需的响应,如果其中任何一个失败,则将它们都回滚? (包括在操作过程中机器崩溃的情况)

请在回答时添加代码示例!我使用的是与文章中相同的技术(nodejs、redis、mongo - 用于数据本身)

谢谢:)

根据您在问题中分享的代码示例,您想要的行为是确保在将幂等键设置到 Redis 中表示此事务已经发生的那一刻和那一刻之间服务器没有崩溃事实上,当事务保存在您的数据库中时。

然而,当同时使用 Redis 和另一个数据库时,你有两个独立的故障点,并且两个动作在不同时刻顺序执行(即使它们同时异步执行也不能保证服务器获胜'在其中任何一个完成之前崩溃)。

你可以做的是在你的交易中包含一个插入语句到 table 保存关于这个请求的相关信息,包括幂等键。由于 ACID 属性确保原子性,它保证事务上的所有语句都成功执行或其中的 none,这意味着如果事务成功,您的幂等性密钥将在您的数据库中可用。

您仍然可以使用 Redis,因为它会提供比数据库更快的结果。

下面提供了一个代码示例,但最好考虑插入到 Redis 和数据库之间的失败与您的业务有多大关系(是否可以用另一种策略来处理?)以避免过度工程。

async function execute(idempotentKey) {
  try {
    // append to the query statement an insert into executions table.
    // this will be persisted with the transaction
    query = ```
        UPDATE firsttable SET ...;
        UPDATE secondtable SET ...;
        INSERT INTO executions (idempotent_key, success) VALUES (:idempotent_key, true);
    ```;

    const db = await dbConnection();
    await db.beginTransaction();
    await db.execute(query);

    // we're setting a key on redis with a value: "false".
    await redisClient.setAsync(idempotentKey, false, 'EX', process.env.KEY_EXPIRE_TIME);

    /*
      if server crashes exactly here, idempotent key will be on redis with false as value.
      in this case, there are two possibilities: commit to database suceeded or not.
      if on next request redis provides a false value, query database to verify if transaction was executed.
    */

    await db.commit();

    // you can now set key value to true, meaning commit suceeded and you won't need to query database to verify that.
    await redis.setAsync(idempotentKey, true);
  } catch (err) {
    await db.rollback();
    throw err;
  }
}