Запретить запросы перезаписывать друг друга - PullRequest
0 голосов
/ 07 ноября 2018

У меня есть приложение, которое получает запросы на бронирование автомобиля, и при бронировании статус автомобиля в таблице cars должен быть установлен на in_use.

Обычно что-то вроде этого будет сделано:

def reserve_car(user_id)
  car = Car.find_by(status: 'available')
  car.update_columns(user_id: user_id, status: 'in_use')

  car
end

Но это решение создает разрыв между выбором доступного автомобиля и обновлением его состояния, и возникает дилемма, когда приложение должно обрабатывать относительно большое количество запросов, поэтому в то же время несколько запросов пытаются зарезервировать один и тот же автомобиль.

Чтобы уменьшить этот риск, я нахожу и обновляю доступную машину в том же SQL-запросе. Также я рандомизирую список доступных автомобилей, чтобы уменьшить его еще больше. Для рандомизации результата я не использую ORDER BY RAND() LIMIT 1, потому что AFAIK генерирует случайный идентификатор для каждой записи, сортирует его и только когда ограничивает результат указанным числом - 1. Что неэффективно, поскольку в таблице автомобилей ожидается большое количество записей. в будущем (100к +).

Итак, я придумаю это решение:

def reserve_car(user_id)
  sql = <<-SQL
    UPDATE
      cars AS r0,
      (
        SELECT
          r1.id
        FROM
          cars AS r1
          JOIN (
            SELECT
              (
                RAND() * (
                  SELECT
                    MAX(id)
                  FROM
                    cars
                )
              ) AS id
          ) AS r2
        WHERE
          r1.status = 'available'
          AND r1.id >= r2.id
        LIMIT
          1
      ) AS r3
    SET
      r0.status = 'in_use',
      r0.user_id = #{ActiveRecord::Base.connection.quote(user_id)}
    WHERE
      r0.id = r3.id
  SQL

  updates = ActiveRecord::Base.connection.exec_update(sql)

  car = Car.find_by(user_id: user_id, status: 'in_use')

  if car.present?
    car
  else
    raise "Failed to reserve car. Updates: #{updates}"
  end
end

Но достаточно часто я получаю исключение "Не удалось зарезервировать автомобиль. Обновления 0", хотя на самом деле я знаю, что есть много доступных автомобилей.

Что может быть не так? Может быть, кто-то может предложить лучшее решение?

Спасибо ?

Ответы [ 2 ]

0 голосов
/ 07 ноября 2018

Если вы используете Rails с активной записью, вы сможете заблокировать запись автомобиля, как только вы ее найдете, чтобы предотвратить ее получение другим запросом. Что-то вроде:

def reserve_car(user_id)
  car = Car.find_by(status: 'available')
  car.with_lock do
    car.update_columns(user_id: user_id, status: 'in_use')
  end

  car
end

Возможно, вам потребуется настроить его в соответствии с Документами блокировки ActiveRecord

0 голосов
/ 07 ноября 2018

Вы не говорите, какая база данных используется, но большинство крупных БД в наши дни имеют способ выполнить обновление и вернуть данные из строки, которая была обновлена ​​

Например, в оракуле:

update car
set in_use = 1
where in_use = 0 and id = (select min(id) from car where in_use = 0)
returning id into car_id_that_was_set_in_use 

Параметр car_id_that_was_set_in_use будет содержать идентификатор автомобиля, который был забронирован

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

MySQL, кажется, является заметным исключением - я не нахожу никаких признаков того, что MySQL поддерживает что-то вроде UPDATE..RETURNING, но есть и другие обходные пути, такие как innodb, поддерживающие SELECT..FOR UPDATE, чтобы позволить вам заблокировать нужную запись обновить и взломать с участием переменных, которые могут выглядеть примерно так:

 UPDATE car SET
     in_use = 1, id = @affectedid := id
 WHERE in_use = 0 AND id=(SELECT MIN(id) FROM car WHERE in_use = 0);
 SELECT @affectedid;

Проверьте это, хотя; Я никогда не использовал это и адаптировал из SO ответа


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

int rowsupdated = 0
int potentialId = -1
while(rowsupdated = 0 and potentialId is not null) {
  potentialId = sql_scalar("SELECT MIN(id) FROM car WHERE in_use = 0")
  rowsupdated = sql_nonquery("UPDATE car SET in_use = 1 WHERE in_use = 0 and id = " + potentialId)
}
if(potentialId is null)
  //there was no car to book, we tried them all - potentialId would only be null if there were no more cars
else
  //potentialId now contains the id of the car we booked

Цикл while будет продолжаться, пока он не закажет машину. Это наивно и неэффективно, но поднимает важный вопрос, применимый также к предыдущему запросу

Запрос на обновление должен ссылаться на то же значение in_use, которое мы все еще ожидаем

Вы не можете выбрать идентификатор, просто идите вперед и установите in_use = 1, не принимая во внимание, установил ли кто-то еще in_use = 1, пока мы находились в режиме ожидания. Это называется оптимистичным параллелизмом - вы НАДЕЖДА, что никто не изменил данные в строке, которую вы хотите редактировать, но вы включили все данные, которые вы знаете о строке, так что если кто-то другой DID изменил строку, обновление завершится неудачно и вернет 0 Записи обновлены. Обновление завершится неудачно, если кто-то еще установит in_use = 1, пока мы бездействуем, и мы ставим условием обновления то, что in_use по-прежнему будет 0 для успешного обновления. Если обновление возвращает 0, мы можем предположить, что кто-то еще изменил строку раньше, чем мы. Затем, зная, что мы не получили этот ряд, мы пытаемся найти другой (или принимаем решение перезаписать / объединить / принять изменения другого человека)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...