Эта страница переведена с английского языковой моделью. При любом расхождении верен английский оригинал.
Захват
Три способа захватить строку: блокировка строки, которую держит открытая транзакция, аренда, записанная в строку, и рекомендательная блокировка на ключе. У каждого способа своя цена в соединениях.
Схема текстом
- Блокировка строк: BEGIN SELECT .. FOR UPDATE SKIP LOCKED; Обработчик работает, транзакция открыта; Оператор завершения, затем COMMIT
- Аренда: UPDATE .. locked_until, attempt + 1 RETURNING; COMMIT сразу: соединение не занято; Обработчик без транзакции; продление раз в полсрока; Завершение WHERE аренда всё ещё своя
- Блокировка ключа: Выбор готовых строк с их ключами; Блокировка ключа на своём соединении, без ожидания; Взятие строки: счёт попытки, чтение; Обработчик работает, ключ заблокирован; Завершение на том же соединении, затем снятие
- BEGIN SELECT .. FOR UPDATE SKIP LOCKED → Обработчик работает, транзакция открыта
- Обработчик работает, транзакция открыта → Оператор завершения, затем COMMIT
- UPDATE .. locked_until, attempt + 1 RETURNING → COMMIT сразу: соединение не занято
- COMMIT сразу: соединение не занято → Обработчик без транзакции; продление раз в полсрока
- Обработчик без транзакции; продление раз в полсрока → Завершение WHERE аренда всё ещё своя
- Выбор готовых строк с их ключами → Блокировка ключа на своём соединении, без ожидания
- Блокировка ключа на своём соединении, без ожидания → Взятие строки: счёт попытки, чтение
- Взятие строки: счёт попытки, чтение → Обработчик работает, ключ заблокирован
- Обработчик работает, ключ заблокирован → Завершение на том же соединении, затем снятие
Форму выбирает описание таблицы. Таблица без заданной формы получает форму блокировки строк;
столбец locked_until выбирает форму аренды; advisory_lock = ".." выбирает форму
рекомендательной блокировки. Формы различаются тем, что держит строку, пока работает её
обработчик, а значит, и тем, что возвращает строку, когда процесс аварийно завершается.
Блокировка строк
Захват открывает транзакцию и выбирает свои строки с FOR UPDATE SKIP LOCKED. Параллельный
захват пропускает заблокированные строки, а не ждёт их. Транзакция остаётся открытой, пока
работает обработчик, и оператор завершения её фиксирует, поэтому строка и её исход меняются в одной
фиксации. Когда процесс аварийно завершается, база данных откатывает его транзакции, и строки
сразу готовы к захвату. Цена - одно соединение на доставку в работе на всё время обработчика. На
MySQL и MariaDB транзакция открывается на уровне READ COMMITTED, если таблица не задаёт уровень,
поэтому захват блокирует строки, которые берёт, а не промежутки между ними.
Аренда
Захват записывает срок в locked_until, засчитывает попытку и сразу фиксируется, поэтому
обработчик работает без открытой транзакции и без занятого соединения. Подписка продлевает каждую
аренду в работе раз в половину срока аренды, поэтому обработчик может работать дольше своей
аренды. Срок служит токеном доставки: завершение называет срок, который держит, и вступает в силу,
только пока строка всё ещё держит этот срок. Обработчик, у которого аренда истекла во время
работы, потому что продления вернули ошибку или прекратились, находит в строке токен другого
захвата, и его завершение возвращает ошибку
SqlxBrokerError::LeaseLost. Когда процесс
аварийно завершается, его строки возвращаются по истечении аренд: через тридцать секунд, если
срок не задан, в целых секундах. Срок вычисляет хост, потому что его сравнивает каждое
завершение, поэтому таблица аренды читает часы хоста.
Рекомендательная блокировка
Захват выбирает готовые строки с их ключами, пытается взять блокировку каждого ключа на
собственном соединении без ожидания, пропускает ключ, который держит другой сеанс, а затем берёт
строку, пока она ещё доступна для захвата. Строки с совпадающими ключами идут в работу по одной,
поэтому advisory_lock = "accounts-{account}" разводит строки каждого счёта, пока строки других
счетов работают. Открытой транзакции нет, но соединение, которое держит блокировку, остаётся у
доставки до её завершения. Когда процесс аварийно завершается, его сеансы заканчиваются, и база
данных снимает их блокировки. Ключ принадлежит базе данных, а не таблице: две таблицы с
совпадающими ключами ждут друг друга.
По базам данных
| PostgreSQL | MySQL 8.0.1+, MariaDB 10.6+ | SQLite | |
|---|---|---|---|
| Блокировка строк | FOR UPDATE SKIP LOCKED | FOR 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.