Как избежать двух одновременных запросов API, нарушающих логику проверки документов?

У меня есть API, который нужно проверить, чтобы вставить новый элемент. Проверка в основном представляет собой средство проверки типа (string, number, Date, e.t.c) и запрашивает базу данных, которая проверяет, есть ли у «пользователя» «элемент» в ту же дату, что, если он это делает, проверка оказывается неудачной.

Псевдокод выглядит так:

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

  })
}

Спасибо.


person Lucas Gomes    schedule 11.04.2019    source источник
comment
Можете ли вы мужчина start_date как unique в своей схеме?   -  person Ashwanth Madhav    schedule 11.04.2019
comment
@AshwanthMadhav Я так не думаю, потому что, пока id_user отличается, два элемента могут иметь одинаковую дату.   -  person Lucas Gomes    schedule 11.04.2019
comment
В loopback SiteUser.validateUniquenessOf('login', { scopedTo: ['siteId'] }) есть опция scopedTo. Так что я думаю, что на вашей платформе может быть вариант.   -  person Ashwanth Madhav    schedule 11.04.2019


Ответы (2)


Объяснять

Это race condition.

два или более потока могут получить доступ к общим данным, и они пытаются изменить их одновременно

Что такое состояние гонки?

Решение:

Есть много способов предотвратить конфликт данных в этом случае, блокировка - это 1 вариант.
Вы можете заблокировать на уровне приложения или на уровне базы данных ... но я предпочитаю, чтобы вы прочитали этот поток, прежде чем выбирать любой из них.

Оптимистическая и пессимистическая блокировка
Быстрое решение: pessimistic-lock https://www.npmjs.com/package/redis-lock

person Đinh Anh Huy    schedule 11.04.2019

Вы должны создать составной индекс или составной первичный ключ, который включает поля id_user и start_date. Это гарантирует, что никакие документы для одного и того же пользователя с одинаковой датой не могут быть созданы, а база данных выдаст ошибку, если вы попытаетесь это сделать. Составной индекс с мангустом

Вы также можете использовать транзакции. Для этого вы должны выполнить find и create методы внутри транзакции, чтобы гарантировать, что никакие параллельные запросы к одному и тому же документу не будут выполняться. Руководство по транзакциям Mongoose

Дополнительная информация

Я бы пошел с уникальным составным индексом, который в вашем конкретном случае должен быть чем-то вроде

mySchema.index({user_id: 1, start_date: 1}, {unique: true});
person Radar155    schedule 11.04.2019
comment
Это хорошее решение для этого случая, но у меня есть коллекции, которые охватывают диапазоны дат, и создание индексов не может быть таким жизнеспособным. Поэтому я выбрал ответ @ Đinh Anh Huy, потому что его можно применить в большем количестве случаев. - person Lucas Gomes; 11.04.2019
comment
Как я уже сказал, блокировка на уровне базы данных в основном означает выполнение изолированной транзакции чтения. При масштабировании блокировка на уровне приложения ненадежна. В любом случае вам понадобится централизованная система запирания (так что база данных - лучший вариант). - person Radar155; 11.04.2019
comment
@ Radar155 Блокировка на уровне приложения также позволяет изолировать данные даже при масштабировании до нескольких служб (в некоторых случаях). Пример: разделить пользователя на службу (пользователь 1-100, служба A, пользователь 101-200, служба B, ...), а затем заблокировать каждое действие пользователя для каждой службы. У каждого решения есть много плюсов и минусов, поэтому подумайте, что мы должны выбрать правильный ответ для правильной проблемы. - person Đinh Anh Huy; 18.04.2019