Могут ли экземпляры модели в приложении Rails быть изменены другими подключениями к БД во время их проверки?
Я углубляюсь в Rails 4 и пытаюсь понять, как безопасно получить доступ к данным модели, когда они доступны через несколько соединений с БД. У меня есть некоторая логика создания совпадений, которая находит самого старого пользователя в очереди, удаляет этого пользователя из очереди и возвращает пользователя...
# UserQueue.rb
class UserQueue < ActiveRecord::Base
has_many :users
def match_user(user)
match = nil
if self.users.count > 0
oldest = self.users.oldest_in_queue
if oldest.id != user.id
self.users.delete(oldest)
match = oldest
end
end
end
end
Если два разных потока выполнили это match_user
метод в то же время, могут ли они найти одинаковые oldest
пользователь и попытаться удалить его из очереди, и вернуть его вызывающей стороне? Если так, как я могу предотвратить это?
Я посмотрел на транзакции, но они, похоже, не являются решением, так как в этом случае изменяется только одна модель (очередь).
Заранее спасибо за вашу мудрость!
2 ответа
ActiveRecord поддерживает блокировку строк.
Это взято из руководства по Rails, блокирующего записи для обновления:
11.1 Оптимистическая блокировка
Оптимистическая блокировка позволяет нескольким пользователям получать доступ к одной и той же записи для редактирования и предполагает минимум конфликтов с данными. Это делается путем проверки того, внес ли другой процесс изменения в запись с момента ее открытия.
ActiveRecord::StaleObjectError
Выдается исключение, если это произошло, и обновление игнорируется.Оптимистическая блокировка колонны
Чтобы использовать оптимистическую блокировку, в таблице должен быть столбец с именем
lock_version
типа целое число. Каждый раз, когда запись обновляется, активная запись увеличиваетlock_version
колонка. Если запрос на обновление сделан с более низким значением вlock_version
поле, чем в настоящее время вlock_version
столбец в базе данных, запрос на обновление завершится неудачно сActiveRecord::StaleObjectError
, Пример:
c1 = Client.find(1)
c2 = Client.find(1)
c1.first_name = "Michael"
c1.save
c2.name = "should fail"
c2.save # Raises an ActiveRecord::StaleObjectError
Затем вы отвечаете за решение конфликта, спасая исключение и откатывая, объединяя или иным образом применяя бизнес-логику, необходимую для разрешения конфликта.
Это поведение можно отключить, установив
ActiveRecord::Base.lock_optimistically = false
,Переопределить имя
lock_version
колонка,ActiveRecord::Base
предоставляет атрибут класса с именемlocking_column
:
class Client < ActiveRecord::Base
self.locking_column = :lock_client_column
end
Я предлагаю прочитать этот раздел в руководстве по Rails.
Да, это может произойти. Как вы предотвратите это, зависит от остальной части вашего приложения /framework/database/etc.
Транзакции не помогут, потому что два клиента могут запустить запрос одновременно, и оба увидят одну и ту же запись UserQueue как самую старую.
Вы хотите мьютекс. Но мьютекс в коде не идеален, если есть другие способы изменения данных (т. Е. Напрямую через SQL и т. Д.). Он также может запутаться, так как в первый раз, когда вы забудете использовать мьютекс, вы открыли состояние гонки. Опять же, может быть, этого достаточно. Просто помните, что ваш мьютекс должен работать между потоками и процессами и т. Д.
Вы можете увидеть, есть ли в вашей базе данных мьютекс или другая блокировка на уровне строк, которую вы можете использовать, чтобы отметить самую старую запись очереди, а затем извлечь ее.
Или найдите другой способ получить самую старую очередь, которая полностью исключает условия гонки. Что-то вроде:
- SQL для обновления самой старой очереди с идентификатором, отличным от user.id, и установкой "mark_for_work" на некоторый уникальный идентификатор.
- Получите строку очереди, чье значение отмеченное_для_работы является нашим уникальным идентификатором.
Вы можете без проблем запускать несколько потоков с помощью описанного выше, поскольку обновление SQL (ну, оно должно быть!) Атомарным.