Перейти к основному содержимому
Нормативная версия документации - английская

Эта страница переведена с английского языковой моделью. При любом расхождении верен английский оригинал.

Захват

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

Три формы захвата, каждая от захвата до завершения: блокировка строк, аренда и рекомендательная блокировкаБлокировка строкАрендаБлокировка ключаBEGINSELECT .. FORUPDATE SKIPLOCKEDОбработчикработает,транзакцияоткрытаОператорзавершения,затем COMMITUPDATE ..locked_until,attempt + 1RETURNINGCOMMIT сразу:соединениене занятоОбработчикбез транзакции;продление разв полсрокаЗавершениеWHERE арендавсё ещёсвояВыбор готовыхстрок с ихключамиБлокировкаключа на своёмсоединении,без ожиданияВзятие строки:счёт попытки,чтениеОбработчикработает,ключзаблокированЗавершениена том жесоединении,затем снятие
Схема текстом
  1. Блокировка строк: BEGIN SELECT .. FOR UPDATE SKIP LOCKED; Обработчик работает, транзакция открыта; Оператор завершения, затем COMMIT
  2. Аренда: UPDATE .. locked_until, attempt + 1 RETURNING; COMMIT сразу: соединение не занято; Обработчик без транзакции; продление раз в полсрока; Завершение WHERE аренда всё ещё своя
  3. Блокировка ключа: Выбор готовых строк с их ключами; Блокировка ключа на своём соединении, без ожидания; Взятие строки: счёт попытки, чтение; Обработчик работает, ключ заблокирован; Завершение на том же соединении, затем снятие
  4. BEGIN SELECT .. FOR UPDATE SKIP LOCKED → Обработчик работает, транзакция открыта
  5. Обработчик работает, транзакция открыта → Оператор завершения, затем COMMIT
  6. UPDATE .. locked_until, attempt + 1 RETURNING → COMMIT сразу: соединение не занято
  7. COMMIT сразу: соединение не занято → Обработчик без транзакции; продление раз в полсрока
  8. Обработчик без транзакции; продление раз в полсрока → Завершение WHERE аренда всё ещё своя
  9. Выбор готовых строк с их ключами → Блокировка ключа на своём соединении, без ожидания
  10. Блокировка ключа на своём соединении, без ожидания → Взятие строки: счёт попытки, чтение
  11. Взятие строки: счёт попытки, чтение → Обработчик работает, ключ заблокирован
  12. Обработчик работает, ключ заблокирован → Завершение на том же соединении, затем снятие

Форму выбирает описание таблицы. Таблица без заданной формы получает форму блокировки строк; столбец locked_until выбирает форму аренды; advisory_lock = ".." выбирает форму рекомендательной блокировки. Формы различаются тем, что держит строку, пока работает её обработчик, а значит, и тем, что возвращает строку, когда процесс аварийно завершается.

Блокировка строк​

Захват открывает транзакцию и выбирает свои строки с FOR UPDATE SKIP LOCKED. Параллельный захват пропускает заблокированные строки, а не ждёт их. Транзакция остаётся открытой, пока работает обработчик, и оператор завершения её фиксирует, поэтому строка и её исход меняются в одной фиксации. Когда процесс аварийно завершается, база данных откатывает его транзакции, и строки сразу готовы к захвату. Цена - одно соединение на доставку в работе на всё время обработчика. На MySQL и MariaDB транзакция открывается на уровне READ COMMITTED, если таблица не задаёт уровень, поэтому захват блокирует строки, которые берёт, а не промежутки между ними.

Аренда​

Захват записывает срок в locked_until, засчитывает попытку и сразу фиксируется, поэтому обработчик работает без открытой транзакции и без занятого соединения. Подписка продлевает каждую аренду в работе раз в половину срока аренды, поэтому обработчик может работать дольше своей аренды. Срок служит токеном доставки: завершение называет срок, который держит, и вступает в силу, только пока строка всё ещё держит этот срок. Обработчик, у которого аренда истекла во время работы, потому что продления вернули ошибку или прекратились, находит в строке токен другого захвата, и его завершение возвращает ошибку SqlxBrokerError::LeaseLost. Когда процесс аварийно завершается, его строки возвращаются по истечении аренд: через тридцать секунд, если срок не задан, в целых секундах. Срок вычисляет хост, потому что его сравнивает каждое завершение, поэтому таблица аренды читает часы хоста.

Рекомендательная блокировка​

Захват выбирает готовые строки с их ключами, пытается взять блокировку каждого ключа на собственном соединении без ожидания, пропускает ключ, который держит другой сеанс, а затем берёт строку, пока она ещё доступна для захвата. Строки с совпадающими ключами идут в работу по одной, поэтому advisory_lock = "accounts-{account}" разводит строки каждого счёта, пока строки других счетов работают. Открытой транзакции нет, но соединение, которое держит блокировку, остаётся у доставки до её завершения. Когда процесс аварийно завершается, его сеансы заканчиваются, и база данных снимает их блокировки. Ключ принадлежит базе данных, а не таблице: две таблицы с совпадающими ключами ждут друг друга.

По базам данных​

PostgreSQLMySQL 8.0.1+, MariaDB 10.6+SQLite
Блокировка строкFOR UPDATE SKIP LOCKEDFOR UPDATE SKIP LOCKEDблокировок строк нет: форма недоступна
Захват арендыодин оператор: блокирующая выборка, обновление найденных строк, возврат строкблокирующая выборка, затем по обновлению на строку, в одной транзакции на READ COMMITTEDодин UPDATE .. RETURNING, без порядка среди возвращённых строк
Рекомендательная блокировкаpg_try_advisory_lock на 64-битном хеше ключа (hashtextextended, PostgreSQL 11+)GET_LOCK(name, 0) по имени базы данных и ключу; имя длиннее 64 символов заменяется его SHA-256ключи хранятся в процессе, один процесс на файл базы данных
Защита группы FIFOтранзакционная блокировка на хеше таблицы и группыблокирующее чтение незавершённых строк группынет: один писатель за раз

Сервер старше нижней границы версии останавливает подписку при старте с ошибкой SqlxBrokerError::ServerTooOld: MySQL 8.0.1 и MariaDB 10.6 - первые выпуски, которые пропускают заблокированные строки. Рекомендательная блокировка PostgreSQL принадлежит сеансу, который её взял, поэтому форме нужно прямое соединение или пулер в режиме сеанса. Два процесса на одном файле SQLite не видят ключей друг друга, и строка тогда может быть доставлена дважды.

В таблице FIFO захват берёт только голову своей группы и ничего не берёт, пока в работе другая строка группы. Защита из таблицы выше блокирует группу в транзакции захвата, поэтому подписки на одну группу на разных брокерах тоже сохраняют её порядок.

Попытка засчитывается там, где форма держит строку: форма блокировки строк засчитывает её при завершении, в операторе, который возвращает строку; форма аренды - при захвате; форма рекомендательной блокировки - при взятии строки. Поэтому аварийное завершение процесса тратит попытку в формах аренды и рекомендательной блокировки и не тратит в форме блокировки строк, где откат отменяет счёт вместе со всем остальным. Доставка сообщает попытку такой, какой она была до захвата, поэтому первая доставка во всех формах читает 1.

Примитивы документируют сами базы данных: PostgreSQL SELECT .. FOR UPDATE и рекомендательные блокировки, MySQL блокирующие чтения и функции блокировок, MariaDB SKIP LOCKED.