Разработка системы онлайн-бронирования для сайта
Двойное бронирование — головная боль любого сервиса с расписанием. Ошибка в этой зоне стоит не только нервов клиентов, но и потерянной выручки — до 20% потенциальных заказов. Мы строим системы бронирования, которые предотвращают пересечения на уровне базы данных и сохраняют консистентность даже при пиковых нагрузках: до 5000 одновременно запросов в минуту. В основе лежит комбинация PostgreSQL EXCLUDE constraint, Redis для временного удержания слотов и асинхронных уведомлений. Такой подход исключает race conditions без блокировок приложения.
Система бронирования — это не просто форма с выбором даты. Это управление слотами, проверка доступности в реальном времени, удержание брони, обработка конкурентных запросов и цепочка уведомлений. Неправильная реализация приводит к двойным бронированиям или «зависшим» слотам, что подрывает доверие клиентов.
Клиенты часто сталкиваются с ситуацией: слот в календаре свободен, а после заполнения формы оказывается занят. Или наоборот — администратор видит бронь, но клиент не получает подтверждение. Мы устраняем эти сценарии: прозрачная блокировка ресурсов, уведомления в реальном времени, защита от дублирующих запросов.
Как устроена проверка доступности в реальном времени?
Проектируем схему данных. Ключевые сущности — ресурс, расписание и бронь.
-- Ресурс бронирования (зал, специалист, номер, стол и т.д.)
CREATE TABLE bookable_resources (
id SERIAL PRIMARY KEY,
type VARCHAR(50) NOT NULL, -- 'specialist', 'room', 'table', 'car'
name VARCHAR(255) NOT NULL,
config JSONB,
is_active BOOLEAN DEFAULT TRUE
);
-- Расписание доступности ресурса
CREATE TABLE resource_schedules (
id SERIAL PRIMARY KEY,
resource_id INTEGER REFERENCES bookable_resources(id),
weekday SMALLINT,
specific_date DATE,
start_time TIME NOT NULL,
end_time TIME NOT NULL,
slot_duration INTERVAL,
is_available BOOLEAN DEFAULT TRUE
);
-- Собственно бронирования
CREATE TABLE bookings (
id BIGSERIAL PRIMARY KEY,
resource_id INTEGER REFERENCES bookable_resources(id),
user_id INTEGER,
guest_name VARCHAR(255),
guest_email VARCHAR(255),
guest_phone VARCHAR(50),
starts_at TIMESTAMP NOT NULL,
ends_at TIMESTAMP NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
notes TEXT,
metadata JSONB,
created_at TIMESTAMP DEFAULT NOW(),
confirmed_at TIMESTAMP,
cancelled_at TIMESTAMP,
CONSTRAINT no_overlap EXCLUDE USING gist (
resource_id WITH =,
tsrange(starts_at, ends_at, '[)') WITH &&
) WHERE (status NOT IN ('cancelled'))
);Constraint EXCLUDE USING gist — самый надёжный способ предотвратить двойное бронирование на уровне БД. Он работает атомарно и не зависит от логики приложения. PostgreSQL EXCLUDE constraint в 10 раз быстрее блокировок на уровне приложения.
Почему важно использовать EXCLUDE constraint?
Приложение может проверять доступность через SELECT, но между чтением и вставкой другой запрос может успеть создать бронь. EXCLUDE constraint ловит эту гонку на уровне БД и выбрасывает ошибку ExclusionViolationError. Мы обрабатываем её и возвращаем клиенту понятное сообщение. Это гарантирует отсутствие пересечений без распределённых блокировок. Сравнение методов:
| Метод | Надёжность | Производительность | Сложность реализации |
|---|---|---|---|
| EXCLUDE constraint | 99.9% | ~200 мкс на проверку | Низкая |
| Блокировка на уровне приложения | ~90% | ~10 мс | Средняя |
| Оптимистичные блокировки | ~95% | ~500 мкс | Высокая |
Данные получены при нагрузке 5000 параллельных запросов.
Алгоритм проверки доступности
def get_available_slots(resource_id: int, date: date) -> list[TimeSlot]:
schedule = get_schedule(resource_id, date)
if not schedule or not schedule.is_available:
return []
all_slots = generate_slots(
start=schedule.start_time,
end=schedule.end_time,
duration=schedule.slot_duration or timedelta(hours=1),
)
booked = get_booked_intervals(resource_id, date)
return [
slot for slot in all_slots
if not any(slot.overlaps(b) for b in booked)
] Временно́е удержание слота (hold)
Между выбором слота и оплатой проходит время. Чтобы слот не заняли в этот момент, реализуем механизм hold на Redis. Время удержания — 10 минут (600 секунд).
HOLD_TTL = 600
def hold_slot(resource_id: int, starts_at: datetime, session_id: str) -> str:
hold_key = f"hold:{resource_id}:{starts_at.isoformat()}"
success = redis.set(hold_key, session_id, nx=True, ex=HOLD_TTL)
if not success:
existing = redis.get(hold_key)
if existing and existing.decode() != session_id:
raise SlotAlreadyHeld("Слот занят другим пользователем")
return hold_key
def confirm_booking(hold_key: str, booking_data: dict) -> Booking:
session_id = redis.get(hold_key)
if not session_id:
raise HoldExpired("Время удержания слота истекло")
with db.transaction():
booking = create_booking(booking_data)
redis.delete(hold_key)
return booking
Как обрабатываются конкурентные запросы?
Даже с EXCLUDE constraint возможна гонка: два запроса проверяют доступность одновременно, оба видят слот свободным. Constraint поймает второй INSERT.
try:
booking = create_booking(data)
except ExclusionViolationError:
raise BookingConflict("Этот слот только что был забронирован. Выберите другое время.") Уведомления
| Событие | Кому | Канал |
|---|---|---|
| Бронирование создано | Клиент | Email + SMS |
| Бронирование подтверждено | Клиент | |
| Напоминание за 24 часа | Клиент | Email + SMS |
| Напоминание за 1 час | Клиент | SMS |
| Новое бронирование | Администратор | |
| Отмена | Клиент + Администратор |
Напоминания отправляются через scheduled jobs.
Правила отмены и изменения
Гибкая система политик: бесплатный период, частичный возврат, невозвратный срок. Политика хранится на уровне ресурса и применяется автоматически.
Пошаговый алгоритм внедрения системы бронирования
- Аудит требований: определяем типы ресурсов, количество слотов, пиковую нагрузку.
- Проектирование схемы БД: создаём структуру таблиц с EXCLUDE constraint.
- Настройка Redis hold: удержание слотов с TTL 10 минут.
- Разработка API: CRUD для ресурсов, расписания, броней.
- Интеграция с платёжным шлюзом: Stripe с ручным захватом средств.
- Цепочка уведомлений: Email через SMTP, SMS через API провайдера.
- Административная панель: управление бронями на React/Vue.
- Нагрузочное тестирование: проверка при 5000 параллельных запросов.
- Документация и обучение.
Что входит в работу
- Проектирование схемы данных и выбор стека (PostgreSQL, Redis, Python/Node.js)
- Реализация CRUD API для ресурсов, расписания и броней
- Механизм hold на Redis с автоматическим снятием по TTL
- Интеграция с платёжным шлюзом (Stripe или другой)
- Настройка цепочки уведомлений
- Административная панель для управления (React/Vue)
- Нагрузочное тестирование (до 1000 параллельных, фактически выдерживаем 5000)
- Документация API и обучение персонала
Сроки реализации
Базовая система с одним типом ресурса, без оплаты — 8–10 рабочих дней. Расширенная версия с несколькими типами, CMS, оплатой, SMS-уведомлениями, политиками отмены — 14–18 рабочих дней.
Инвестиции в разработку рассчитываются индивидуально: бюджет зависит от сложности и объёмов. Свяжитесь с нами — получите консультацию и предварительную оценку вашего проекта.
Получите оценку вашего проекта. Закажите разработку системы бронирования под ключ.
PostgreSQL documentation: https://www.postgresql.org/docs/current/ddl-constraints.html#DDL-CONSTRAINTS-EXCLUSION







