DEV Community

Cover image for Advisory locks (рекомендательные блокировки)
Ivan Shamatov
Ivan Shamatov

Posted on • Edited on

Advisory locks (рекомендательные блокировки)

не путать с другими более оптимистичными или менее пессимистичными блокировками

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

Начнем с того, что лок делать НЕ умеет

1. Этот лок не блокирует данные, записи, таблицы, которые вы модифицируете внутри блока кода
Если у вас есть вот такой кусочек кода, который выполняется в потоке А

User.with_advisory_lock("updating_users_#{users.ids.join}") do
  users.where(role: "admin").update(role: "user")
end
Enter fullscreen mode Exit fullscreen mode

и вот такой кусочек кода, который выполняется в потоке Б,

User.join(:permisson).where(permissions: { update: true }).update(role: "admin")
Enter fullscreen mode Exit fullscreen mode

то поток Б может модифицировать данные, которые модифицирует поток А, и наоборот.

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Так а зачем же тогда нужен этот лок?

Эта блокировка отлично подходит для контроля конкурентных запросов на уровне приложения (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
Enter fullscreen mode Exit fullscreen mode

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

Top comments (2)

Collapse
 
glebson1988 profile image
Gleb

с одним и тем же ключОм
😉

Collapse
 
abuhtoyarov profile image
Buhtoyarov Artem

Ухты, спасибо. Раньше использовал громоздкую конструкцию с мьютексом и сохранением ключика в редис. Этот метод выглядит более лаконично.