Двойная бронь: как гонка в записи была поймана и закрыта EXCLUDE-констрейнтом

Двойная бронь — одна из тех проблем, которые рано или поздно всплывают в любом сервисе с расписанием, переговорками или билетами. Два пользователя одновременно пытаются забронировать один и тот же слот, и система, не рассчитанная на конкурентный доступ, создаёт две записи. Казалось бы, достаточно перед записью проверить, свободно ли время, но между проверкой и вставкой проходит достаточно времени, чтобы второй запрос успел выполнить ту же проверку. Возникает состояние, которое разработчики называют гонкой (race condition).

Недавняя статья на Habr рассказывает, как одна команда поймала такую гонку в своей системе бронирования и закрыла её с помощью EXCLUDE-констрейнта — ограничения базы данных, которое запрещает пересечение временных интервалов. Разберём, что это за механизм, почему он оказался эффективнее классических блокировок и как его можно применить на практике.

Что такое гонка в записи

Представьте два параллельных запроса: А и Б. Оба обращаются к таблице броней, чтобы проверить, свободно ли время с 12:00 до 13:00 в комнате №4. База данных в этот момент не содержит записей на этот интервал, поэтому оба запроса получают ответ «свободно». Затем запрос А вставляет бронь, а следом запрос Б делает то же самое. В результате в системе образуются две одинаковые брони, хотя каждое из действий по отдельности выглядело корректным.

Это классический пример гонки — состояния, при котором итоговый результат зависит от того, в каком порядке выполняются конкурентные операции. Проблема усугубляется тем, что при обычной проверке кода её не видно: всё работает, пока запросы не приходят одновременно. В статье на Habr авторы описывают, как именно им удалось воспроизвести проблему и убедиться, что дело не в баге логики, а в отсутствии защиты на уровне базы.

Как обычно решают проблему двойной брони

Существует несколько стандартных подходов, но у каждого есть ограничения.

  1. Уникальный индекс на слот. Если бронь закреплена за конкретным временем и комнатой, можно создать уникальный индекс на пару полей (room_id, time). Однако в большинстве реальных систем бронирование работает с диапазонами — «с 12 до 13», а не с точечным значением. Уникальный индекс на диапазон не сработает, потому что диапазоны могут перекрываться частично.

  2. Пессимистичная блокировка — SELECT FOR UPDATE. Этот способ блокирует строку до конца транзакции. Но если брони ещё нет, блокировать нечего — приходится блокировать всю таблицу или использовать процедурные блокировки, что снижает пропускную способность системы.

  3. Оптимистичная блокировка с версией строки. Здесь при обновлении сравнивается версия записи. Проблема в том, что при вставке новой записи сравнивать не с чем, поэтому требуется дополнительная проверка и повторная попытка при конфликте.

  4. Уровень изоляции SERIALIZABLE. Он даёт строгие гарантии, но требует обработки ошибок сериализации и повторного выполнения транзакций. На практике это усложняет приложение.

Каждый из этих методов имеет право на жизнь, но в статье, о которой идёт речь, разработчики остановились на более элегантном решении — EXCLUDE-констрейнте.

EXCLUDE-констрейнт: защита прямо в базе

EXCLUDE — это специальное ограничение, которое есть в PostgreSQL. В отличие от UNIQUE, оно работает не только с равенством, но и с произвольными операторами, например с пересечением диапазонов. Если в таблице создано такое ограничение, база сама не даст вставить строки, которые нарушают условие.

Для бронирования временных интервалов типичное определение выглядит так:

CREATE TABLE bookings (
    id serial PRIMARY KEY,
    room_id int NOT NULL,
    start_time timestamptz NOT NULL,
    end_time timestamptz NOT NULL,
    EXCLUDE USING gist (
        room_id WITH =,
        tsrange(start_time, end_time) WITH &&
    )
);

Здесь tsrange — диапазон времени, а оператор && означает «пересекается». Ограничение гарантирует, что для одной и той же комнаты не может быть двух броней с пересекающимися временными интервалами. Если две транзакции попытаются вставить такие записи, одна из них получит ошибку ещё на уровне базы.

Важно отметить, что для работы EXCLUDE-констрейнта используется GiST-индекс. Он позволяет быстро проверять пересечение интервалов и не создаёт заметной нагрузки на производительность, даже при большом количестве записей.

Как авторы статьи поймали гонку

В материале на Habr рассказывается, что проблема была обнаружена не сразу — первые симптомы появились при нагрузочном тестировании. Два одновременных запроса создавали брони на одно и то же время, и только анализ логов показал, что оба запроса прошли проверку доступности до момента вставки. Команда воспроизвела ситуацию, отправив несколько параллельных HTTP-запросов, и убедилась, что гонка воспроизводится стабильно.

После рассмотрения различных вариантов — от блокировок до перехода на SERIALIZABLE — разработчики решили добавить EXCLUDE-констрейнт. Это решение оказалось простым и надёжным: вся логика защиты сосредоточена в схеме базы данных, а не в коде приложения, и не требует ручной координации между запросами.

Какие преимущества у такого решения

Сравним EXCLUDE-констрейнт с традиционными подходами.

Критерий Проверка в приложении Блокировка EXCLUDE-констрейнт
Гарантия корректности Зависит от сложности кода Только при правильном использовании Обеспечивается базой
Производительность Не влияет на БД Блокирует даже непересекающиеся записи Работает только при реальном конфликте
Сложность внедрения Нужно писать логику Нужно управлять транзакциями Одно определение в DDL
Обработка конфликтов Ручная Ручная Ошибка возникает автоматически

Как видно из таблицы, главный плюс — надёжность. Разработчики перестают зависеть от того, не забыл ли кто-то вызвать блокировку в нужном месте. База данных сама не допускает некорректных пересечений.

Практические рекомендации

Если вы используете PostgreSQL в проектах с бронированием, EXCLUDE-констрейнт — один из самых эффективных инструментов для борьбы с гонками. Чтобы внедрить его, нужно:

  • убедиться, что в проекте установлено расширение btree_gist, если вы комбинируете обычный тип room_id с диапазоном времени;
  • правильно выбрать тип диапазона: tsrange, tstzrange или daterange — в зависимости от того, нужно ли учитывать часовой пояс;
  • продумать обработку ошибки уникальности (SQLSTATE 23P01) на уровне приложения, чтобы пользователь увидел понятное сообщение.

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

Для читателей, которые работают с PostgreSQL и хотят автоматизировать обмен данными с этой базой, платформа ASI Biont поддерживает подключение к PostgreSQL через API — подробнее на asibiont.com/courses.

Вывод

Гонка в записи — классическая проблема конкурентного доступа, которая не всегда проявляется на этапе разработки, но даёт о себе знать при реальной нагрузке. Статья на Habr показывает, как EXCLUDE-констрейнт помог команде избавиться от двойной брони простым и декларативным способом. Вместо того чтобы эмулировать защиту в коде, разработчики перенесли её в саму схему базы данных, и это позволило гарантировать корректность на уровне СУБД.

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

← Все статьи

Комментарии

Читайте также

AI-лаборатории жмут на тормоза, а Amazon и SpaceX — на газ: парадокс августа 2026

1 августа 2026

Best in Class: как стримить лучшие игры и учиться на одном ноутбуке с GeForce NOW

1 августа 2026

React — современный фронтенд: как QA-инженеру удвоить доход и выйти на новый уровень

1 августа 2026

Progressive Web Components: эволюция веб-стандартов и новый этап развития интерфейсов

1 августа 2026

Smallest.ai привлекла $13 млн: как сверхбыстрый голосовой ИИ приближает нас к «человеческому» звучанию

1 августа 2026

ROS 2 + AI: подключаем робота к ASI Biont — управление через Telegram без сложного кода

1 августа 2026

Метрика, ты что?! Всё верно, но я забыл про реорганизацию: как не потерять данные веб-аналитики при изменениях на сайте

1 августа 2026

Soft Skills и Карьера: как прокачать гибкие навыки с помощью AI-обучения и получить работу мечты

1 августа 2026

Интеграция OPC-UA (SCADA, DCS) с AI-агентом ASI Biont: пошаговый гайд по автоматизации промышленности без кода

1 августа 2026