如何同时处理多个 post 请求,同时将其中一个请求保存在数据库中?
How to handle multiple post requests at the same time while saving one of them on the db?
我从网络钩子收到 n post 个请求(在每个网络钩子触发器上)。来自同一触发器的所有请求的数据都是相同的——它们都具有相同的 '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 仍然 equels null,因此它最终会创建更多相同的订单。一个(不是很好的解决方案)是使 orderId 在数据库中唯一,这可以防止创建具有相同 orderId 的订单,但无论如何都会尝试,这会导致数据库中的空 ID 递增。任何想法将不胜感激。
p.s。如您所见,我尝试添加 'sleep' 函数但无济于事。
您的数据库未能在下一个请求到达之前完成其保存操作。该问题类似于 Dogpile Effect 或“缓存冲击”。
这需要更多地考虑您是如何构建问题的:换句话说,“解决方案”将更具哲学性,并且可能与代码关系不大,因此您在 Whosebug 上的结果可能会有所不同。
“休眠”解决方案根本不是解决方案:无法保证数据库操作可能需要多长时间,也无法保证在另一个重复请求到达之前您可能需要等待多长时间。根据经验,任何时候将“睡眠”部署为并发问题的“解决方案”,通常都是错误的选择。
让我提出两种可能的处理方式:
选项 1: 只写:即在写入之前不要尝试通过从数据库中读取来“解决”这个问题。只是让通往数据库的管道尽可能地愚蠢并继续写入。例如。考虑一个“日志记录”table,它只存储 webhook 抛给它的任何内容——不要试图从中读取,只是继续插入(或更新插入)。如果你得到 100 个关于特定订单的 ping-backs,那就这样吧:你的 table 会记录所有内容,如果你最终得到一个 orderId
的 100 行,让其他下游流程担心如何处理所有重复数据。据推测,Sequelize 足够聪明(并且您的数据库支持任何进程锁定)来排队操作并处理写入重复。
如果您确实希望对 orderId
进行唯一约束,那么此处的 upsert
操作会有所帮助(这似乎是明智的,但您可能会意识到特定设置中的其他注意事项) .
选项 2: 使用队列。这显然更复杂,因此请仔细权衡您的用例是否值得额外的工作。不是立即将数据写入数据库,而是将 webhook 数据放入队列(例如先进先出 FIFO 队列)。理想情况下,您会希望选择一个支持重复数据删除的队列,以便保证退出的消息是唯一的,但它会推断状态,并且通常依赖于某种数据库,这就是开始时的问题。
队列为您做的最重要的事情是它会序列化消息,这样您就可以一次处理一条消息(而不是同时启动多个数据库操作)。当您从队列中读取消息时,您可以将数据更新到数据库中。如果 webhook 持续触发并且更多消息进入队列,那很好,因为队列强制它们全部排列成单个文件,您可以一次处理每个插入。你会知道每个数据库操作在它移动到下一条消息之前已经完成,所以你永远不会“猛击”数据库。换句话说,在数据库前面放置一个队列将允许它在数据库准备就绪时处理数据,而不是在 webhook 调用时处理数据。
此处队列的概念与 semaphore 所完成的类似。请注意,您的数据库接口可能已经在后台实现了一种 queue/pool,因此请仔细权衡此选项:不要重新发明轮子。
希望这些想法有用。
你节省了我的时间@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
});
我从网络钩子收到 n post 个请求(在每个网络钩子触发器上)。来自同一触发器的所有请求的数据都是相同的——它们都具有相同的 '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 仍然 equels null,因此它最终会创建更多相同的订单。一个(不是很好的解决方案)是使 orderId 在数据库中唯一,这可以防止创建具有相同 orderId 的订单,但无论如何都会尝试,这会导致数据库中的空 ID 递增。任何想法将不胜感激。 p.s。如您所见,我尝试添加 'sleep' 函数但无济于事。
您的数据库未能在下一个请求到达之前完成其保存操作。该问题类似于 Dogpile Effect 或“缓存冲击”。
这需要更多地考虑您是如何构建问题的:换句话说,“解决方案”将更具哲学性,并且可能与代码关系不大,因此您在 Whosebug 上的结果可能会有所不同。
“休眠”解决方案根本不是解决方案:无法保证数据库操作可能需要多长时间,也无法保证在另一个重复请求到达之前您可能需要等待多长时间。根据经验,任何时候将“睡眠”部署为并发问题的“解决方案”,通常都是错误的选择。
让我提出两种可能的处理方式:
选项 1: 只写:即在写入之前不要尝试通过从数据库中读取来“解决”这个问题。只是让通往数据库的管道尽可能地愚蠢并继续写入。例如。考虑一个“日志记录”table,它只存储 webhook 抛给它的任何内容——不要试图从中读取,只是继续插入(或更新插入)。如果你得到 100 个关于特定订单的 ping-backs,那就这样吧:你的 table 会记录所有内容,如果你最终得到一个 orderId
的 100 行,让其他下游流程担心如何处理所有重复数据。据推测,Sequelize 足够聪明(并且您的数据库支持任何进程锁定)来排队操作并处理写入重复。
如果您确实希望对 orderId
进行唯一约束,那么此处的 upsert
操作会有所帮助(这似乎是明智的,但您可能会意识到特定设置中的其他注意事项) .
选项 2: 使用队列。这显然更复杂,因此请仔细权衡您的用例是否值得额外的工作。不是立即将数据写入数据库,而是将 webhook 数据放入队列(例如先进先出 FIFO 队列)。理想情况下,您会希望选择一个支持重复数据删除的队列,以便保证退出的消息是唯一的,但它会推断状态,并且通常依赖于某种数据库,这就是开始时的问题。
队列为您做的最重要的事情是它会序列化消息,这样您就可以一次处理一条消息(而不是同时启动多个数据库操作)。当您从队列中读取消息时,您可以将数据更新到数据库中。如果 webhook 持续触发并且更多消息进入队列,那很好,因为队列强制它们全部排列成单个文件,您可以一次处理每个插入。你会知道每个数据库操作在它移动到下一条消息之前已经完成,所以你永远不会“猛击”数据库。换句话说,在数据库前面放置一个队列将允许它在数据库准备就绪时处理数据,而不是在 webhook 调用时处理数据。
此处队列的概念与 semaphore 所完成的类似。请注意,您的数据库接口可能已经在后台实现了一种 queue/pool,因此请仔细权衡此选项:不要重新发明轮子。
希望这些想法有用。
你节省了我的时间@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
});