не путать с другими более оптимистичными или менее пессимистичными блокировками
Возможно у вас в проекте уже используются эти локи, но вы никогда не задумывались, что это за локи, как они работают и, самое важное, как НЕ работают.
Эта блокировка в качестве аргумента принимает строку, которая будет служить уникальным ключом.
Начнем с того, что лок делать НЕ умеет
1. Этот лок не блокирует данные, записи, таблицы, которые вы модифицируете внутри блока кода
Если у вас есть вот такой кусочек кода, который выполняется в потоке А
User.with_advisory_lock("updating_users_#{users.ids.join}") do
users.where(role: "admin").update(role: "user")
end
и вот такой кусочек кода, который выполняется в потоке Б,
User.join(:permisson).where(permissions: { update: true }).update(role: "admin")
то поток Б может модифицировать данные, которые модифицирует поток А, и наоборот.
2. Нельзя заменить транзакцию локом.
Рекомендательные блокировки (в постгресе) так вообще игнорируют границы транзакции если запрос на уровне сессии. Чтобы получить эффект "все или ничего" нужно использовать транзакцию, даже если используешь и лок. Например, вот так:
User.transaction do
User.with_advisory_lock("updating_user_#{user.id}") do
user.update(role: "admin")
user.user_organization.first.update_periods
end
end
3. В общем случае, ключ для блокировок разных частей кода должен быть разным.
Вот два метода, и оба используют блокировку с одним и тем же ключом. Да, смотрится очень эффектно, но смысла в этом мало, потому как методы абсолютно независимые. Нет никакого резона, чтобы метод "update" ждал, пока метод "check" закончит свою работу, даже если он выполняется параллельно. И наоборот.
def update(users)
with_user_lock do
users.where(role: "admin").update(role: "user")
end
end
def check(users)
with_user_lock do
users.all? { _1.task_completed? }
end
end
def with_user_lock(user, &block)
User.with_advisory_lock("updating_user_#{user.id}", &block)
end
Конечно, есть исключения и можно придумать сценарий, когда один и тот же лок в разных методах имеет смысл, но вы должны четко понимать, что вы делаете и зачем.
Так а зачем же тогда нужен этот лок?
Эта блокировка отлично подходит для контроля конкурентных запросов на уровне приложения (Application-level Concurrency Control). Это мьютекс, который используется для того, чтобы гарантировать, что два разных процесса, которые исполняют ОДИН и ТОТ ЖЕ код, не выполняют этот код одновременно.
class UpdateRoleJob < ApplicationJob
def perform(user)
User.with_advisory_lock("updating_user_#{user.id}") do
user.update(role: "admin")
end
end
end
Если по какой-то причине у вас в очереди появилось две задачи для одного и того же пользователя, и если случилось так, что 2 разных воркера начнут обрабатывать эти две задачи, то вы можете быть уверены, что только один из них будет выполняться в данный момент, а второй будет ждать, пока первый закончит свою работу.
Top comments (2)
Ухты, спасибо. Раньше использовал громоздкую конструкцию с мьютексом и сохранением ключика в редис. Этот метод выглядит более лаконично.