Защитите последнее место на мастер-классе

PostgreSQLBeginner
Практиковаться сейчас

Введение

Два оператора продают места на мастер-класс по рукоделию. Гость ожидает подтверждённого места, а не счётчика, который лишь выглядит неотрицательным. Сначала вы оформите обычное групповое бронирование, затем воспроизведёте два подтверждения на одно место в отдельном учебном мастер-классе. Наконец, вы защитите это место, не мешая другому мастер-классу продолжать приём гостей.

Нужны базовые навыки навигации в терминале и знание PostgreSQL SELECT, WHERE, INSERT и UPDATE. Транзакции, идентификация соединений, блокировки строк, RETURNING и восстановление объясняются здесь. PostgreSQL и синтетические данные подготовлены. Workshop Desk — командная оболочка; Desk A и Desk B — отдельные соединения штатного клиента psql с одной базой данных на одной машине. Держите все три вкладки открытыми. Текст и метки работающих инструментов — на английском. Чтение и обычные ошибки включены в оценку длительности, данную автором; это не исследование времени прохождения новичками.

Добавляйте только названные пробные бронирования и обновляйте places_left соответствующих мастер-классов. Упражнение с конкурирующими запросами явно разрешает удаление собственных пробных бронирований. Сохраните исходные бронирования, ID, названия, вместимость и заметки мастер-классов, определения таблиц и подготовленные инструменты. Не меняйте настройки сервера и не создавайте таблицы. В упражнении используется уровень изоляции PostgreSQL READ COMMITTED в одной базе данных; платежи, повторные попытки и взаимоблокировки между несколькими мастер-классами выходят за его рамки.

Оформите обычное групповое бронирование

На этом шаге вы свяжете настоящее подтверждение группы с числом мест, которое она занимает.

Начните во вкладке Workshop Desk, в командной оболочке. ./ запускает программу из текущей папки. desk status читает настоящие таблицы, а не исправляет их. Первый результат перечисляет workshop_id|title|capacity|places_left|booked_seats. Следующий — request_code|workshop_id|seats|desk. Вертикальные черты разделяют поля.

./desk status

Мастер-класс 101 имеет вместимость 5, одно исходное забронированное место и 4 свободных места. У мастер-класса 102 одно место и нет бронирований. Сохраните original-family на мастер-классе 101 в точности как есть.

Переключитесь на Desk A. Приглашение Desk A workshops=> означает, что вы находитесь в клиенте PostgreSQL psql, подключённом к базе данных workshops. Здесь вводите SQL, а не команды оболочки. Точка с запятой завершает инструкцию SQL. Соединение — это сеанс общения с сервером базы данных; pg_backend_pid() идентифицирует его серверный процесс. Другое значение в Desk B будет обозначать независимое соединение.

SELECT pg_backend_pid(), current_user;

Пользователь — workshop_write; ID процесса меняется. BEGIN начинает транзакцию: группу изменений, которые становятся видимыми вместе при COMMIT. До этого другое соединение не видит ваше новое бронирование. ROLLBACK отбрасывает незафиксированные изменения. В этом первом примере другой оператор не продаёт места на мастер-класс 101.

RETURNING показывает строки, действительно изменённые UPDATE. Вычтите 2 места и вставьте соответствующее бронирование на два места в той же транзакции. Текст использует одинарные кавычки; код запроса однозначно идентифицирует это бронирование.

BEGIN;
UPDATE workshops
SET places_left = places_left - 2
WHERE workshop_id = 101
RETURNING workshop_id, places_left;
INSERT INTO bookings (request_code, workshop_id, seats, desk)
VALUES ('normal-group', 101, 2, 'A');

Обновление возвращает 101 | 2. Приглашение содержит *, указывая на открытую транзакцию. Переключитесь на Workshop Desk и изучите зафиксированный реестр:

./desk status

Он всё ещё показывает 4 места и только исходное бронирование на мастер-класс 101. Вернитесь на Desk A и завершите:

COMMIT;

Переключитесь на Workshop Desk:

./desk status

Теперь у 101 осталось 2 места и забронировано 3: одно исходное плюс ваша группа из двух человек. Её фактическое бронирование — normal-group|101|2|A. Успешная транзакция сохраняет capacity = places_left + booked_seats. Первая проверка читает эти факты работы системы, а не делает вывод о том, как вы их вводили.

Если вы допустите опечатку в SQL внутри транзакции, приглашение может измениться на !. Тогда транзакция находится в состоянии ошибки: дальнейший SQL будет завершаться ошибкой до ROLLBACK;. Выполните откат в той же вкладке оператора, изучите ./desk status в Workshop Desk и повторите только работу, которая не была зафиксирована. Никогда не повторяйте уже зафиксированный запрос вслепую. Конфликт кода запроса — ошибка, а не второе подтверждение.

Воспроизведите решение по устаревшим данным о наличии мест

На этом шаге вы намеренно создадите некорректный учебный реестр, увидите, почему неотрицательного счётчика недостаточно, и ограничите ущерб мастер-классом 102.

Переключитесь на Desk A. Этот эксперимент намеренно записывает устаревшее число вместо вычисления условного выделения места. Не используйте его как итоговый метод бронирования. Уровень изоляции READ COMMITTED даёт каждой инструкции представление зафиксированных данных на момент начала этой инструкции; предшествующий SELECT не резервирует свой результат.

BEGIN ISOLATION LEVEL READ COMMITTED;
SELECT workshop_id, places_left FROM workshops WHERE workshop_id = 102;

Переключитесь на Desk B. Убедитесь, что ID его процесса отличается от Desk A, затем начните собственную транзакцию и прочитайте то же место:

SELECT pg_backend_pid(), current_user;
BEGIN ISOLATION LEVEL READ COMMITTED;
SELECT workshop_id, places_left FROM workshops WHERE workshop_id = 102;

Оба SELECT возвращают одно место. Ни один SELECT его не зарезервировал. Переключитесь на Desk A и сохраните решение, основанное на прежнем числе:

UPDATE workshops SET places_left = 0 WHERE workshop_id = 102;
INSERT INTO bookings (request_code, workshop_id, seats, desk)
VALUES ('unsafe-A', 102, 1, 'A');

Оставьте транзакцию A открытой. Переключитесь на Desk B и введите только этот UPDATE:

UPDATE workshops SET places_left = 0 WHERE workshop_id = 102;

Приглашение пока не возвращается. Это штатное ожидание блокировки, а не потеря соединения. A изменил эту строку и удерживает её блокировку до завершения транзакции. Переключитесь на Workshop Desk:

./desk waits

У настоящего серверного процесса Desk B поле wait_event_type равно Lock, а blocked_by содержит ID процесса A. ID меняются. Наблюдатель только читает активность PostgreSQL; никакая анимация или таймер не создаёт это состояние.

Переключитесь на Desk A:

COMMIT;

Переключитесь на Desk B. Теперь его UPDATE завершается. Он всё ещё может установить уже нулевой остаток в ноль, потому что его условие WHERE указывает только мастер-класс. Добавьте его подтверждение и зафиксируйте:

INSERT INTO bookings (request_code, workshop_id, seats, desk)
VALUES ('unsafe-B', 102, 1, 'B');
COMMIT;

Переключитесь на Workshop Desk:

./desk status

Теперь мастер-класс 102 имеет вместимость 1, остаток мест 0 и 2 забронированных места. Существуют и unsafe-A, и unsafe-B. Остаток удовлетворил проверке неотрицательности базы данных, но одному гостю места нет. Одно лишь ожидание не исправило устаревшее решение. Следующий шаг удаляет только эти явно одноразовые пробы и заменяет решение. Проверка шага принимает это учебное состояние или правильно исправленное итоговое; она не использует историю терминала как доказательство вашего исследования.

Выделите место перед его подтверждением

На этом шаге вы потребуете фактического условного выделения места перед записью подтверждения и восстановите состояние после отмены ожидающего запроса.

Переключитесь на Desk A. DELETE удаляет только строки, выбранные условием WHERE. IN указывает два одноразовых кода запросов; не удаляйте исходное или обычное бронирование. В той же транзакции восстановите только учебный мастер-класс 102:

BEGIN;
DELETE FROM bookings WHERE request_code IN ('unsafe-A', 'unsafe-B');
UPDATE workshops SET places_left = 1 WHERE workshop_id = 102;
COMMIT;

Сначала отработайте отмену ожидания блокировки на отдельном мастер-классе 104. A отменит своё изменение через ROLLBACK, поэтому эта проба не создаёт бронирования:

BEGIN;
UPDATE workshops SET places_left = 0 WHERE workshop_id = 104;

Переключитесь на Desk B:

BEGIN;
UPDATE workshops SET places_left = places_left - 1
WHERE workshop_id = 104 AND places_left >= 1
RETURNING workshop_id;

Пока B ждёт, нажмите Ctrl+C в Desk B. PostgreSQL сообщает canceling statement due to user request с SQLSTATE 57014. SQLSTATE — пятисимвольная категория ошибки PostgreSQL. Отменённая инструкция оставляет эту явную транзакцию в состоянии ошибки. Продемонстрируйте отличие от обычного пустого результата:

SELECT 1;

Это ожидаемая ошибка: current transaction is aborted, SQLSTATE 25P02. Восстановите состояние в Desk B:

ROLLBACK;

Переключитесь на Desk A и отмените учебное изменение:

ROLLBACK;

Теперь защитите настоящее последнее место на мастер-классе 102. Условие WHERE проверяет достаточность мест как часть самого UPDATE. После ожидания конкурирующего обновления READ COMMITTED повторно проверяет это условие по обновлённой строке. RETURNING сообщает, действительно ли эта транзакция выделила место.

В Desk A:

BEGIN ISOLATION LEVEL READ COMMITTED;
UPDATE workshops SET places_left = places_left - 1
WHERE workshop_id = 102 AND places_left >= 1
RETURNING workshop_id, places_left;

A получает 102 | 0: место выделено ему. Только потому, что вернулась строка, создайте соответствующее подтверждение:

INSERT INTO bookings (request_code, workshop_id, seats, desk)
VALUES ('safe-last-A', 102, 1, 'A');

Оставьте его незафиксированным. Переключитесь на Desk B:

BEGIN ISOLATION LEVEL READ COMMITTED;
UPDATE workshops SET places_left = places_left - 1
WHERE workshop_id = 102 AND places_left >= 1
RETURNING workshop_id, places_left;

B ждёт. Переключитесь на Workshop Desk и прочитайте настоящее ожидание:

./desk waits

Блокировка строки не должна останавливать не связанные с ней мастер-классы. Оставаясь в Workshop Desk, откройте кратковременное третье соединение. ./db --desk A использует тот же подготовленный вход в базу данных; этот временный вызов из оболочки независим от уже открытой вкладки Desk A. Here-document с маркером в кавычках отправляет этому клиенту все строки до завершающего SQL, затем закрывает его; держите завершающий маркер на отдельной строке. Сохранённая в бронировании C обозначает этот третий запрос.

./db --desk A <<'SQL'
BEGIN;
UPDATE workshops SET places_left = places_left - 1
WHERE workshop_id = 103 AND places_left >= 1
RETURNING workshop_id, places_left;
INSERT INTO bookings (request_code, workshop_id, seats, desk)
VALUES ('other-workshop', 103, 1, 'C');
COMMIT;
SQL

Он возвращает 103 | 2 и фиксируется, пока строка мастер-класса 102 всё ещё удерживается. Переключитесь на Desk A:

COMMIT;

Переключитесь на Desk B. Теперь его UPDATE возвращает ноль строк и UPDATE 0: новый нулевой остаток не удовлетворяет places_left >= 1. Это успешная инструкция SQL без выделения места, а не ошибка SQL. Не вставляйте подтверждение. Завершите транзакцию B:

ROLLBACK;

Переключитесь на Workshop Desk:

./desk status

У мастер-класса 102 одно бронирование, safe-last-A|102|1|A, и ноль мест. У мастер-класса 103 есть other-workshop|103|1|C и два места. У мастер-класса 104 по-прежнему исходное одно место и нет бронирования; 101 сохраняет обычную группу и исходную семью. Теперь каждый мастер-класс удовлетворяет уравнению баланса.

При обычном ошибочном подтверждении новой пробы сначала выполните ROLLBACK любой открытой транзакции в той же вкладке оператора. В этом учебном окружении разрешено удалить только собственный ошибочный код и пересчитать только остаток его мастер-класса в одной транзакции. Например, если вы случайно зафиксировали typo-trial на мастер-классе 103, выполните следующее в Desk A, затем изучите состояние перед повтором нужного запроса. Подзапрос суммирует фактически забронированные места; COALESCE(..., 0) использует ноль, если бронирований не осталось. Это явное учебное восстановление, а не часть алгоритма бронирования:

BEGIN;
DELETE FROM bookings WHERE request_code = 'typo-trial' AND workshop_id = 103;
UPDATE workshops
SET places_left = capacity -
  (SELECT COALESCE(SUM(seats), 0) FROM bookings WHERE workshop_id = 103)
WHERE workshop_id = 103;
COMMIT;

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

Резюме

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