如何避免两个并发的 API 请求破坏文档验证背后的逻辑?

Posted

技术标签:

【中文标题】如何避免两个并发的 API 请求破坏文档验证背后的逻辑?【英文标题】:How to avoid two concurrent API requests breaking the logic behind document validation? 【发布时间】:2019-09-01 21:34:40 【问题描述】:

我有一个 API,为了插入一个需要验证的新项目。验证基本上是一个类型验证器(stringnumberDate 等)并查询数据库以检查“用户”是否在同一日期有一个“项目”,如果它进行验证是不成功。

伪代码如下:

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"
)


两者都会成功,这是不应该的,因为“用户”不能在同一日期拥有两个“项目”。

如果我在第一个完成后执行第二个,一切都会按预期进行。

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()

  )


谢谢。

【问题讨论】:

你能把男性 start_date 作为 unique 在你的架构中吗? @AshwanthMadhav 我不这么认为,因为只要id_user 不同,两个项目就可以有相同的日期。 在环回SiteUser.validateUniquenessOf('login', scopedTo: ['siteId'] )有一个选项scopedTo。所以我认为您的平台中可能有一个选项。 【参考方案1】:

解释

那是race condition

两个或多个线程可以访问共享数据并尝试同时更改它

What is a race condition?

解决办法:

在这种情况下有很多方法可以防止冲突数据,锁是一种选择。 您可以锁定应用程序级别或数据库级别...但我更喜欢您在选择其中任何一个之前阅读此线程。

Optimistic vs. Pessimistic locking 快速解决方案:pessimistic-lockhttps://www.npmjs.com/package/redis-lock

【讨论】:

【参考方案2】:

您应该创建包含id_userstart_date 字段的复合索引复合主键。这将确保不能为同一用户创建具有相同日期的文档,并且如果您尝试这样做,数据库将引发错误。 Composite index with mongoose

您也可以使用事务。为此,您应该在事务中执行findcreate 方法,以确保不会执行对同一文档的并发查询。 Mongoose transactions tutorial

More infos

我会选择一个唯一的复合索引,在你的具体情况下应该是这样的

mySchema.index(user_id: 1, start_date: 1, unique: true);

【讨论】:

对于这种情况,这是一个很好的解决方案,但我有包含日期​​范围的集合,并且创建索引不可行。因此我选择了@Đinh Anh Huy 的答案,因为它可以应用于更多情况。 “在数据库级别锁定”基本上意味着进行隔离的读取事务,正如我所说的。当您扩展时,“在应用程序级别锁定”是不可靠的。无论如何,您都需要一个集中式锁定系统(因此数据库是更好的选择)。 @Radar155 锁定应用程序级别也是使数据隔离,即使您扩展到多个服务(在某些情况下)。例如:每个服务的分片用户(用户 1-100 服务 A,用户 101-200 服务 B,...)然后锁定每个用户对每个服务的操作。每种解决方案都有很多优点和缺点,所以认为我们必须为正确的问题选择正确的答案。

以上是关于如何避免两个并发的 API 请求破坏文档验证背后的逻辑?的主要内容,如果未能解决你的问题,请参考以下文章

极验高并发验证服务背后的技术实现

使用过期令牌发出同时 API 请求时如何避免多个令牌刷新请求

当 HTTP 方法是 Apache .htaccess 上的 OPTIONS 时,如何避免请求基本身份验证?

如何避免使用 Visual Studio 和 docker 容器在 API 上出现连接被拒绝错误?

实战并发编程 - 07循环等待&死锁问题

如何避免反射和序列化破坏单例模式