【问题标题】:How to handle multiple post requests at the same time while saving one of them on the db?如何同时处理多个发布请求,同时将其中一个保存在数据库中?
【发布时间】:2022-01-12 21:34:29
【问题描述】:

我从 webhook 收到 n 个 post 请求(在每个 webhook 触发器上)。来自同一触发器的所有请求的数据都是相同的——它们都具有相同的“orderId”。我有兴趣只保存其中一个请求,因此在每个端点命中时,我都会检查这个特定的 orderId 是否作为我的数据库中的一行存在,否则 - 创建它。

if (await orderIdExists === null) {
                await Order.create(
                    {
                        userId,
                        status: PENDING,
                        price,
                        ...
                    }
                );
                await sleep(3000)
                function sleep(ms) {
                    return new Promise((resolve) => {
                        setTimeout(resolve, ms);
                    });
                }
            }
            return res.status(HttpStatus.OK).send({success: true})
        } catch (error) {
            return res.status(HttpStatus.INTERNAL_SERVER_ERROR).send({success: false})
        }
    }
    else {
        return res.status(HttpStatus.UNAUTHORIZED).send(responseBuilder(false, responseErrorCodes.INVALID_API_KEY, {}, req.t));
    }

}

问题是在 Sequelize 设法将新创建的订单保存在数据库中之前(所有 n 个 post 请求都在 1 秒内到达终点 - 或更短),我已经从其他 n 个 post 请求中获得了另一个端点命中,而orderIdExists 仍然等于 null,因此它最终会创建更多相同的订单。一个(不是很好的解决方案)是使 orderId 在数据库中唯一,这可以防止创建具有相同 orderId 的订单,但无论如何都会尝试,这会导致数据库中的空 id 递增。任何想法将不胜感激。 p.s.如您所见,我尝试添加“睡眠”功能无济于事。

【问题讨论】:

    标签: node.js express sequelize.js


    【解决方案1】:

    您的数据库未能在下一个请求到达之前完成其保存操作。问题类似于Dogpile Effect 或“缓存猛击”。

    这需要更多地思考您是如何构建问题的:换句话说,“解决方案”将更具哲学性,并且可能与代码的关系较少,因此您在 StackOverflow 上的结果可能会有所不同。

    “睡眠”解决方案根本不是解决方案:无法保证数据库操作可能需要多长时间,或者在另一个重复请求到达之前您可能等待多长时间。根据经验,任何时候将“睡眠”部署为并发问题的“解决方案”,通常都是错误的选择。

    让我提出两种可能的处理方式:

    选项 1: 只写:即在写入之前不要尝试通过从数据库中读取来“解决”这个问题。只要保持通向数据库的管道尽可能地笨拙并继续编写。例如。考虑一个“日志”表,它只存储 webhook 向它抛出的任何内容——不要尝试从中读取,只需继续插入(或更新插入)。如果您收到关于特定订单的 100 个 ping-backs,就这样吧:您的表将记录所有内容,如果您最终得到 100 行单个 orderId,让其他下游进程担心如何处理所有这些重复的数据。据推测,Sequelize 足够智能(并且您的数据库支持任何进程锁定)来排队操作并处理重复写入。

    如果您确实想对 orderId 设置唯一约束,则此处的 upsert 操作会很有帮助(这似乎很合理,但您可能会意识到特定设置中的其他注意事项)。

    选项 2: 使用队列。这显然更复杂,因此请仔细权衡您的用例是否证明了额外工作的合理性。不是立即将数据写入数据库,而是将 webhook 数据放入队列(例如先进先出 FIFO 队列)。理想情况下,您会希望选择一个支持重复数据删除的队列,以保证现有消息是唯一的,但它会推断状态,并且通常依赖于某种数据库,这就是问题所在。

    队列为您做的最重要的事情是它会序列化消息,这样您就可以一次处理一个消息(而不是同时启动多个数据库操作)。当您从队列中读取消息时,您可以将数据插入到数据库中。如果 webhook 不断触发并且更多消息进入队列,那很好,因为队列会强制它们全部排列单个文件,并且您可以一次处理每个插入。您将知道每个数据库操作在移至下一条消息之前已完成,因此您永远不会“猛击”数据库。换句话说,在数据库前面放置一个队列将允许它在数据库准备就绪时处理数据,而不是在 webhook 调用时处理数据。

    这里的队列概念类似于semaphore 所完成的。请注意,您的数据库接口可能已经在底层实现了一种队列/池,因此请仔细权衡此选项:不要重新发明轮子。

    希望这些想法有用。

    【讨论】:

    • 非常感谢,它们确实非常有用!但是有一个问题,如果每个 http 请求都是无状态的,我该如何实现队列?即它如何“知道”先前的请求并将它们全部存储在队列中?
    • 一些队列保留状态(有时由 DATABASE 支持,尽管可能是 更快 数据库)。在大多数开发场景中保存状态的工具是数据库,因此即使队列只对消息进行排序,这也允许数据库一次处理一个,这意味着它可以正确检查状态(即如果该行存在)一次一条消息。换句话说,这是您不希望并发的地方:您希望一次处理一条消息。
    【解决方案2】:

    你节省了我的时间@Everett 和@april-henig。我发现直接保存到数据库读取以记录重复项。如果您将记录存储到一个对象中并一次处理一条记录对我有很大帮助。 也许我会分享我的解决方案,也许有些人将来会觉得它有用。

    创建一个空对象保存成功请求

    export const queueAllSuccessCallBack = {};
    

    POST 请求保存在对象中

        if (status === 'success') { // I checked the request if is only successfully
          const findKeyTransaction = queueAllSuccessCallBack[client_reference_id];
          if (!findKeyTransaction) { // check if Id is not added to avoid any duplicates
            queueAllSuccessCallBack[client_reference_id] = {
              transFound,
              body,
            }; // save new request id as key and the value as data you want
          }
        }
    

    访问要保存到数据库的对象

       const keys = Object.keys(queueAllSuccessCallBack);
       keys.forEach(async (key) => {
         ...  
         // Do extra checks if you want to do so
        // Or save in database direct
       });
    

    【讨论】:

      猜你喜欢
      • 2021-12-21
      • 2018-01-19
      • 1970-01-01
      • 2012-09-13
      • 2020-08-06
      • 1970-01-01
      • 2021-08-17
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多