Как избежать одновременной покупки билетов на одно место
Что проверяют
Понимаете ли вы, откуда берётся гонка: два запроса одновременно читают «место свободно» и оба его продают. Тестировщику важно знать механизмы защиты и уметь доказать тестом, что она работает. Следом почти всегда спрашивают: «А как бы вы это проверили?»
Как отвечать
Коротко назовите, чем защищаются на стороне сервера:
- атомарное условное обновление:
UPDATE seats SET status = 'sold' WHERE id = ? AND status = 'free'. Если обновилось 0 строк, место уже занято; - уникальное ограничение:
UNIQUE (event_id, seat_id)в таблице билетов, второйINSERTупадёт, сколько бы запросов ни пришло; - пессимистичная блокировка:
SELECT ... FOR UPDATEвнутри транзакции, второй запрос ждёт первого; - оптимистичная блокировка: колонка
version, обновление проходит только при совпадении версии; - временная бронь: место резервируется на время оплаты, например на 15 минут, и освобождается по таймауту.
Мьютекс в коде приложения не спасает, если сервис запущен в нескольких экземплярах: у каждого свой мьютекс.
Дальше главное для QA: как это проверить. Последовательные запросы из Postman гонку не воспроизведут, нужна настоящая параллельность: JMeter с Synchronizing Timer, k6 или короткий скрипт на потоках, который отправляет 50 одинаковых запросов на одно место одновременно. Ожидаемый результат: ровно один успешный ответ, остальные получают понятную ошибку (например, 409 Conflict), в базе одна продажа, деньги списаны один раз. Отдельно проверьте повтор оплаты с тем же ключом идемпотентности, истечение брони и обрыв связи между бронью и оплатой.
Где спотыкаются
Предлагают проверить руками в двух вкладках браузера. Между кликами проходят сотни миллисекунд, а окно гонки обычно измеряется единицами, так что тест проходит всегда и ничего не доказывает.
Верят, что «проверка перед покупкой» решает проблему. Проверка и запись отдельными шагами без блокировки и есть та самая гонка: оба запроса успевают увидеть «свободно».
Смотрят только на ответы API. После теста нужно проверить базу: количество билетов на место, статусы, платежи. Иначе можно пропустить случай, когда второй запрос вернул ошибку, но запись всё равно успел создать.