От идеи до production-ready решения с учётом конкуренции, блокировок и отказоустойчивости
Цель: Создать простой, но надёжный микросервис для онлайн-бронирования мест в кинотеатре, устойчивый к одновременным запросам от множества пользователей.
План статьи
- Постановка задачи
- System Design: архитектура и компоненты
- Аналитика: модели данных, диаграммы, инструменты
- Реализация (бэкенд + фронтенд)
- Проблемы и их решение: конкурентность, дублирование, отказоустойчивость
- Заключение и возможные улучшения
1. Постановка задачи
Требования:
- Кинотеатр показывает фильмы с 10:00 до 00:00 по МСК.
- Сеансы начинаются каждые 2 часа: 10:00, 12:00, 14:00, ..., 22:00 → 7 сеансов в день.
- В зале 10 рядов × 10 мест = 100 мест.
- Пользователь: выбирает фильм,
выбирает дату и сеанс,
выбирает конкретное место (ряд + номер),
подтверждает бронирование. - Билет нельзя продать дважды.
- Фронтенд — простой HTML + формы, без Thymeleaf, React и т.п.
- Бэкенд — Spring Boot на Java 11.
- Данные хранятся в PostgreSQL.
Пример списка фильмов (на 1 месяц):
Название Жанр Длительность
Интерстеллар Sci-Fi 169 мин
Паразиты Драма 132 мин
Матрица Экшен 136 мин
Остров собак Анимация 101 мин
Дюна Sci-Fi 155 мин
Фильмы идут ежедневно весь месяц.
2. System Design
Архитектура: Monolith vs Microservice?
Для простоты и обучения — единый Spring Boot микросервис (не распределённая система), но спроектированный так, чтобы его можно было легко разделить позже.
Компоненты:
[HTML Frontend]
↓ (HTTP POST/GET)
[Spring Boot App]
↓
[PostgreSQL DB]
Endpoints:
- GET /movies — список фильмов
- GET /showtimes?date=2026-02-01&movieId=1 — сеансы на дату
- GET /seats?showtimeId=5 — свободные/занятые места
- POST /book — забронировать место
3. Аналитика: модели данных и диаграммы
Сущности:
Film {
id: Long
title: String
duration: Integer // минуты
}
Showtime {
id: Long
filmId: Long
dateTime: LocalDateTime // например, 2026-02-01T10:00
}
Seat {
id: Long
row: Integer // 1–10
number: Integer // 1–10
}
Booking {
id: UUID
showtimeId: Long
seatId: Long
bookedAt: LocalDateTime
status: BookingStatus // CONFIRMED, EXPIRED
}
Важно: Seat — не привязан к залу, потому что у нас один зал. При масштабировании — добавим hallId.
Диаграмма связей (ERD)
Можно нарисовать в:
- draw.io (diagrams.net) — бесплатно, онлайн
- Lucidchart
- PlantUML (если любите код)
Пример ERD (текстово):
Film ────< Showtime >─── Booking ─── Seat
- Один фильм → много сеансов
- Один сеанс → много бронирований
- Одно место → может быть забронировано только один раз на сеанс
4. Реализация
Технологии:
- Java 11
- Spring Boot 2.7.x (поддержка Java 11)
- Spring Web, Spring Data JPA
- PostgreSQL
- HikariCP (пул соединений)
- Lombok (для геттеров/сеттеров)
4.1. Entity-классы
4.2. Репозитории
4.3. Контроллер бронирования
4.4. Простой HTML-фронтенд (static/indexcinema.html)
5. Проблемы и их решение
❗ Проблема 1: Гонка за место (Race Condition)
Сценарий:
Два пользователя одновременно проверяют existsBy... → оба получают false → оба сохраняют бронь → одно место продано дважды.
✅ Решение: Оптимистическая или пессимистическая блокировка
Вариант: UNIQUE-ограничение в БД (самый надёжный!)
Добавьте в таблицу bookings уникальный индекс:
ALTER TABLE bookings
ADD CONSTRAINT uk_showtime_seat UNIQUE (showtime_id, seat_id);
Теперь, если два потока попытаются вставить одну и ту же пару — второй получит DataIntegrityViolationException.
Обрабатываем в контроллере:
Проблема 2: Бронь без оплаты → места "зависают"
Пользователь забронировал, но не оплатил → место недоступно 2 часа.
✅ Решение: временная бронь + фоновая очистка
- Добавьте поле expiresAt в Booking.
- При бронировании: expiresAt = now() + 10 минут.
- Запускайте @Scheduled задачу каждые 5 минут:
@Scheduled(fixedRate = 300_000) // 5 минут
public void cleanupExpiredBookings() {
bookingRepo.deleteByExpiresAtBefore(LocalDateTime.now());
}
Это требует поля expiresAt и cron-очистки, но предотвращает "заморозку" мест.
6. Заключение и возможные улучшения
Вы создали минимальный, но надёжный микросервис для продажи билетов, который:
- Предотвращает двойную продажу через UNIQUE-ограничение,
- Имеет чистую архитектуру,
- Готов к расширению (залы, оплата, email-уведомления).
Что можно добавить дальше:
- REST API для мобильного приложения
- WebSocket для обновления карты мест в реальном времени
- Интеграция с платежной системой
- Кэширование списка сеансов (Redis)
- Docker + Docker Compose для развёртывания
Заключение
Пример, рассмотренный в статье, можно найти по адресу:
https://github.com/ShkrylAndrei/blog_yandex/tree/main/src/main/java/info/examples/cinema