如何避免两个并发 API 请求破坏文档验证背后的逻辑?
How to avoid two concurrent API requests breaking the logic behind document validation?
我有一个 API,为了插入一个新项目,它需要被验证。验证基本上是一个类型验证器(string
、number
、Date
、e.t.c)并查询检查 "user" 是否具有 [=42] 的数据库=] 在同一日期,如果这样做则验证不成功。
伪代码如下:
const Item = require("./models/item");
function post(newDoc){
let errors = await checkForDocErrors(newDoc)
if (errors) {
throw errors;
}
let itemCreated = await Item.create(newDoc);
return itemCreated;
}
我的问题是如果我像这样执行两个并发请求:
const request = require("superagent");
// Inserts a new Item
request.post('http://127.0.0.1:5000/api/item')
.send({
"id_user": "6c67ea36-5bfd-48ec-af62-cede984dff9d",
"start_date": "2019-04-02",
"name": "Water Bottle"
})
/*
Inserts a new Item, which shouldn't do. Resulting in two items having the
same date.
*/
request.post('http://127.0.0.1:5000/api/item')
.send({
"id_user": "6c67ea36-5bfd-48ec-af62-cede984dff9d",
"start_date": "2019-04-02",
"name": "Toothpick"
})
两者都会成功,这不应该是因为 "user" 不能在同一日期有两个 "items"。
如果我在第一个完成后执行第二个,一切都会按预期进行。
request.post('http://127.0.0.1:5000/api/item') // Inserts a new Item
.send({
"id_user": "6c67ea36-5bfd-48ec-af62-cede984dff9d",
"start_date": "2019-04-02",
"name": "Water Bottle"
})
.then((res) => {
// It is not successful since there is already an item with that date
// as expected
request.post('http://127.0.0.1:5000/api/item')
.send({
"id_user": "6c67ea36-5bfd-48ec-af62-cede984dff9d",
"start_date": "2019-04-02",
"name": "Toothpick"
})
})
为了避免这种情况,我发送了一个包含一系列文档的请求,但我想防止这个问题或至少降低发生的可能性。
解决方案
我创建了一个redis服务器。使用包 redis-lock
并环绕 POST
路由。
var client = require("redis").createClient()
var lock = require("redis-lock")(client);
var itemController = require('./controllers/item');
router.post('/', function(req, res){
let userId = "";
if (typeof req.body === 'object' && typeof req.body.id_user === 'string') {
userId = req.body.id_user;
}
lock('POST ' + req.path + userId, async function(done){
try {
let result = await itemController.post(req.body)
res.json(result);
} catch (e) {
res.status(500).send("Server Error");
}
done()
})
}
谢谢。
您应该创建一个复合索引或一个复合主键,其中包括id_user
和start_date
字段。这将确保不会为同一用户创建具有相同日期的文档,如果您尝试这样做,数据库将抛出错误。
Composite index with mongoose
您也可以使用 交易。为此,您应该在事务中执行 find
和 create
方法,以确保不会执行对同一文档的并发查询。
Mongoose transactions tutorial
我会选择一个唯一的复合索引,在你的具体情况下应该是这样的
mySchema.index({user_id: 1, start_date: 1}, {unique: true});
解释一下
那是一个race condition
.
two or more threads can access shared data and they try to change it at the same time
What is a race condition?
解决方案:
在这种情况下有很多方法可以防止数据冲突,锁是一种选择。
您可以锁定应用程序级别或数据库级别...但我更希望您在选择其中任何一个之前阅读此线程。
Optimistic vs. Pessimistic locking
快速解决方案:pessimistic-lock
https://www.npmjs.com/package/redis-lock
我有一个 API,为了插入一个新项目,它需要被验证。验证基本上是一个类型验证器(string
、number
、Date
、e.t.c)并查询检查 "user" 是否具有 [=42] 的数据库=] 在同一日期,如果这样做则验证不成功。
伪代码如下:
const Item = require("./models/item");
function post(newDoc){
let errors = await checkForDocErrors(newDoc)
if (errors) {
throw errors;
}
let itemCreated = await Item.create(newDoc);
return itemCreated;
}
我的问题是如果我像这样执行两个并发请求:
const request = require("superagent");
// Inserts a new Item
request.post('http://127.0.0.1:5000/api/item')
.send({
"id_user": "6c67ea36-5bfd-48ec-af62-cede984dff9d",
"start_date": "2019-04-02",
"name": "Water Bottle"
})
/*
Inserts a new Item, which shouldn't do. Resulting in two items having the
same date.
*/
request.post('http://127.0.0.1:5000/api/item')
.send({
"id_user": "6c67ea36-5bfd-48ec-af62-cede984dff9d",
"start_date": "2019-04-02",
"name": "Toothpick"
})
两者都会成功,这不应该是因为 "user" 不能在同一日期有两个 "items"。
如果我在第一个完成后执行第二个,一切都会按预期进行。
request.post('http://127.0.0.1:5000/api/item') // Inserts a new Item
.send({
"id_user": "6c67ea36-5bfd-48ec-af62-cede984dff9d",
"start_date": "2019-04-02",
"name": "Water Bottle"
})
.then((res) => {
// It is not successful since there is already an item with that date
// as expected
request.post('http://127.0.0.1:5000/api/item')
.send({
"id_user": "6c67ea36-5bfd-48ec-af62-cede984dff9d",
"start_date": "2019-04-02",
"name": "Toothpick"
})
})
为了避免这种情况,我发送了一个包含一系列文档的请求,但我想防止这个问题或至少降低发生的可能性。
解决方案
我创建了一个redis服务器。使用包 redis-lock
并环绕 POST
路由。
var client = require("redis").createClient()
var lock = require("redis-lock")(client);
var itemController = require('./controllers/item');
router.post('/', function(req, res){
let userId = "";
if (typeof req.body === 'object' && typeof req.body.id_user === 'string') {
userId = req.body.id_user;
}
lock('POST ' + req.path + userId, async function(done){
try {
let result = await itemController.post(req.body)
res.json(result);
} catch (e) {
res.status(500).send("Server Error");
}
done()
})
}
谢谢。
您应该创建一个复合索引或一个复合主键,其中包括id_user
和start_date
字段。这将确保不会为同一用户创建具有相同日期的文档,如果您尝试这样做,数据库将抛出错误。
Composite index with mongoose
您也可以使用 交易。为此,您应该在事务中执行 find
和 create
方法,以确保不会执行对同一文档的并发查询。
Mongoose transactions tutorial
我会选择一个唯一的复合索引,在你的具体情况下应该是这样的
mySchema.index({user_id: 1, start_date: 1}, {unique: true});
解释一下
那是一个race condition
.
two or more threads can access shared data and they try to change it at the same time
What is a race condition?
解决方案:
在这种情况下有很多方法可以防止数据冲突,锁是一种选择。
您可以锁定应用程序级别或数据库级别...但我更希望您在选择其中任何一个之前阅读此线程。
Optimistic vs. Pessimistic locking
快速解决方案:pessimistic-lock
https://www.npmjs.com/package/redis-lock