使 POST 请求幂等

问题描述 投票:0回答:2

我一直在寻找一种方法来设计我的 API,使其具有幂等性,这意味着其中一些方法是为了使我的 POST 请求路由幂等,我偶然发现了 this 文章。

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

其中对总体思路有很好的解释。但缺少的是他自己实现的一些例子。

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

本质上,在他的代码示例中有两种情况,

如果一切顺利的话流程:

  • 在数据库上打开一个事务,保存需要通过 POST 请求更改的数据
  • 在此交易中,执行所需的更改
  • 在Redis存储中设置
    Idempotency-key
    键和值,这是对客户端的响应
  • 为该键设置过期时间
  • 提交交易

代码内部出现问题时的流程:

  • 并且函数流程内发生异常。
  • 执行事务回滚

注意,打开的事务是针对某个DB的,我们称他为A。 但是,这与他也使用的redis存储无关,这意味着事务的回滚只会影响DB A。

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

但是,如果运行代码的机器崩溃了,而它已经执行了

Set expire time to that key
并且现在即将运行事务的提交,会发生什么?

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

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

这个问题有什么解决办法吗?

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

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

谢谢:)

rest http post distributed idempotent
2个回答
6
投票

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

但是,当 Redis 和另一个数据库一起使用时,您会遇到两个独立的故障点,并且两个操作在不同时刻顺序执行(即使它们同时异步执行,也不能保证服务器不会崩溃)其中任何一个已完成)。

您可以做的是在事务中包含一条插入语句,该语句指向保存此请求的相关信息(包括幂等键)的表。由于 ACID 属性确保原子性,它保证事务上的所有语句都成功执行,或者都不执行,这意味着如果事务成功,您的幂等性密钥将在数据库中可用。

您仍然可以使用 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;
  }
}

© www.soinside.com 2019 - 2024. All rights reserved.