Часть 12 из 16

Уже можно отметить билет как использованный, но турникет должен сделать ещё и запись в журнале. Если первое изменение сохранится, а второе нет, история прохода окажется неполной. В этой части объединим обе операции транзакцией InnoDB и разберём, что дают START TRANSACTION, COMMIT и ROLLBACK.

Мы также учтём особенность работы через браузер: команды одной транзакции нужно отправить одним SQL-пакетом в одном соединении, иначе обещанная атомарность потеряется.

Журнал проходов

CREATE TABLE checkin_log (
 checkin_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
 sale_id BIGINT UNSIGNED NOT NULL,
 scanned_at DATETIME NOT NULL,
 gate_name VARCHAR(60) NOT NULL,
 PRIMARY KEY (checkin_id),
 FOREIGN KEY (sale_id) REFERENCES ticket_sales(sale_id)
) ENGINE=InnoDB;

Таблицы операции должны использовать транзакционный InnoDB.

Выполняем билетную операцию одним пакетом

В phpMyAdmin отправьте весь блок ниже за один запуск во вкладке SQL. Не нажимайте «Выполнить» отдельно после START TRANSACTION: последовательные веб-запросы могут использовать разные соединения, а блокировка и транзакция должны жить в одном соединении.

START TRANSACTION;
UPDATE ticket_sales
SET checked_in_at='2026-10-30 18:02:00'
WHERE ticket_code='FEST00000001' AND checked_in_at IS NULL;
SET @ticket_marked = ROW_COUNT();
INSERT INTO checkin_log (sale_id,scanned_at,gate_name)
SELECT sale_id,'2026-10-30 18:02:00','Северный вход'
FROM ticket_sales
WHERE ticket_code='FEST00000001' AND @ticket_marked=1;
COMMIT;
SELECT @ticket_marked AS ticket_was_marked;

Если билет отсутствует или уже отмечен, UPDATE изменит ноль строк, а INSERT SELECT не добавит запись в журнал. Результат в конце покажет 1 при успешной отметке или 0, если билет не изменился. В рабочем приложении ошибки нужно обрабатывать и откатывать незавершённую транзакцию.

Блокировки требуют коротких операций

FOR UPDATE блокирует найденную строку до конца транзакции. Завершайте её быстро, иначе параллельные операции будут ждать. Следом сохраним многократно используемый отчёт через представление.

Почему весь сценарий отправляется одним запросом

Транзакция относится к одному соединению MySQL. Если отправить START TRANSACTION, затем отдельным кликом UPDATE, а потом ещё одним запросом INSERT, веб-интерфейс может открыть разные соединения. Тогда блокировка и границы транзакции не будут такими, как вы ожидаете.

Поэтому учебный пакет в предыдущем блоке запускается целиком из одной вкладки SQL.

Внутри пакета UPDATE пытается изменить только билет, где дата прохода ещё NULL. Сразу после UPDATE функция ROW_COUNT() сохраняет число изменённых строк в переменную соединения. INSERT ...

SELECT создаёт запись журнала только если эта цифра равна 1. Для отсутствующего или уже использованного кода вставка в журнал не произойдёт.

Что гарантирует InnoDB

Атомарность означает, что изменения внутри незавершённой транзакции не становятся постоянными до COMMIT. Если один из запросов завершается ошибкой, не фиксируйте частичный результат: выполните ROLLBACK в том же соединении или закройте его так, чтобы незавершённая транзакция откатилась.

Изоляция и блокировки защищают конкурирующие изменения, но долгие транзакции удерживают ресурсы.

Откройте журнал и билет после завершения пакета. При успешной первой попытке найдите одну запись прохода; повторный запуск должен показать ticket_was_marked = 0 и не добавлять второй лог. Такой тест проверяет не только синтаксис, но и защиту от повторного сканирования.

ACID на примере отметки билета

Транзакции InnoDB дают четыре свойства, которые часто объединяют словом ACID. Атомарность — оба изменения вместе или ни одного; согласованность — ограничения остаются истинными; изоляция — параллельные операции не должны считать один билет новым одновременно; долговечность — после COMMIT результат сохраняется.

В нашем примере уникальный код и условное UPDATE помогают защитить билет от повторного прохода.

Функция ROW_COUNT() важна именно сразу после UPDATE: она возвращает количество изменённых строк последней команды. Если поменять порядок и вызвать другую команду до чтения значения, результат уже будет относиться к ней. Переменная @ticket_marked живёт в текущем соединении, поэтому весь пакет выполняем одним отправлением.

Что проверять после выполнения

При успешной отметке запрос в конце покажет 1, а в checkin_log появится одна запись. При втором запуске должно показать 0 и не добавить второй проход. Проверьте обе таблицы SELECT-запросами в отдельной сессии после COMMIT.

Если тестируете ошибку, делайте это на вымышленных данных; реальный журнал проходов должен быть неизменяемой историей событий.

Порядок работы с транзакцией

  1. Проверьте тестовый билет заранее.
  2. Запустите весь SQL-пакет одним выполнением во вкладке phpMyAdmin.
  3. Сверьте ticket_was_marked: 1 означает новую отметку, 0 — повторный или отсутствующий билет.
  4. После COMMIT прочитайте билет и журнал, чтобы подтвердить оба изменения.

Так мы не оставляем транзакцию открытой между отдельными нажатиями и видим не только успешный ответ интерфейса, но и сохранённый результат.

← Предыдущая статья: Ограничения и индексы MySQL: защищаем данные и запросы

Следующая статья: Представления MySQL: сохраняем повторяемый отчёт →