cd ..

Concert Booking

1만 명 대기열과 1,000석 예매를 가정한 시스템에서 3가지 락 전략을 비교했고, Redis 분산 락 기준 Mixed Load 1,005 RPS와 overselling 0건을 검증했습니다.

1,005 RPS
Mixed Load

Redis 분산 락 기준 혼합 트래픽 처리량

0건
Overselling

Hot seat contention에서 초과 예약 없이 1건만 성공

100%
Distributed Reservation

재고 50개 동시 예약에서 Redis 분산 락 성공률

Java 21Spring Boot 3Spring SecuritySpring Data JPAJWTPostgreSQLRedisRedissonApache KafkaSSEDockerTestcontainersk6
Key Result

Scenario A Hot Seat Contention: 3가지 전략 모두 overselling 0건, 동일 좌석 100명 동시 요청에서 정확히 1건만 성공했습니다.

개요

콘서트 예매 시스템에서 가장 어려운 문제는 초과 예약 없이 처리량을 유지하는 것입니다. 이 프로젝트에서는 비관적 락, 낙관적 락, Redis 분산 락을 같은 시나리오로 비교하고, Redis Sorted Set + SSE 대기열과 Kafka 기반 만료·취소 이벤트까지 연결했습니다. 전략별 성공률과 p95를 k6로 다시 측정해 어떤 방식이 실제 혼합 트래픽에서 유리한지 숫자로 확인한 프로젝트입니다.

동시성 전략 비교

  • Scenario A: 동일 좌석 100명 동시 요청에서 3가지 전략 모두 overselling 0건, 정확히 1건만 성공
  • Scenario B: 서로 다른 좌석 50개 예약에서 비관적 락 100%, 낙관적 락 40%, Redis 분산 락 100% 성공
  • Scenario C: Mixed Load에서 Redis 분산 락이 1,005 RPS, 읽기 p95 7ms, 쓰기 p95 6ms로 가장 안정적
  • 낙관적 락은 concert_schedule.availableSeats @Version 충돌로 성공률이 40%까지 떨어지는 병목을 확인

대기열 & 좌석 점유

  • Redis Sorted Set + SSE로 대기열 순번을 스트리밍하고, 100등 이내 사용자에게 입장 토큰 발급
  • 입장 토큰은 1회 사용 후 소멸하고 TTL 5분으로 만료 처리
  • 좌석 임시 점유는 Redis TTL 5분으로 관리하고, 미결제 예매는 스케줄러가 자동 해제
  • ShedLock으로 다중 인스턴스 환경에서도 만료 스케줄러가 중복 실행되지 않도록 설계

이벤트 & 복구

  • Kafka reservation.completed / reservation.cancelled 이벤트로 결제 완료와 취소·만료 흐름 분리
  • seat-release consumer가 만료·취소 좌석을 복구하고 Redis 재고를 되돌림
  • manual commit + 3회 재시도 + DLT로 실패 이벤트를 격리
  • Redisson MultiLock으로 다좌석 예매 시에도 분산 환경 잠금을 일관되게 유지

검증 환경

  • PostgreSQL 16, Redis 7, Kafka KRaft, Docker Compose 기반 로컬 통합 환경
  • Testcontainers와 k6 시나리오 A/B/C로 정합성·성공률·레이턴시를 비교 측정
  • Apple M4, HikariCP 10, Tomcat max-threads 200 환경에서 성능 결과 수집
  • 성능 결과와 설계를 docs/PERF_RESULT.md, docs/DESIGN.md로 문서화

아키텍처

전체 아키텍처

ERD

전체 ERD

시퀀스 다이어그램

대기열 진입
예매 & 결제
만료 좌석 해제

문제 원인

  1. 01같은 좌석에 동시 요청이 몰리면 overselling 없이 정합성을 지켜야 했습니다.
  2. 021만 명 수준의 동시 접속을 가정하면 대기열 없이 예매 API를 열기 어렵습니다.
  3. 03결제하지 않은 예매가 남아 있으면 좌석이 계속 잠겨 재고가 복구되지 않습니다.
  4. 04분산 환경에서는 락, 이벤트 처리, 스케줄러가 모두 다중 인스턴스를 전제로 동작해야 했습니다.

해결 과정

  1. 01비관적 락, @Version + Spring Retry 기반 낙관적 락, Redis + Redisson 분산 락 3가지 전략을 구현해 같은 시나리오에서 비교했습니다.
  2. 02Redis Sorted Set + SSE로 대기열을 만들고, 100등 이내 사용자에게만 입장 토큰을 발급했습니다.
  3. 03Redis TTL 5분과 ReservationExpirationScheduler를 연결하고, ShedLock으로 스케줄러 중복 실행을 막았습니다.
  4. 04Kafka reservation.completed / reservation.cancelled 이벤트, manual commit, 3회 재시도, DLT로 복구 경로를 구성했습니다.

결과

  1. 01Scenario A Hot Seat Contention: 3가지 전략 모두 overselling 0건, 동일 좌석 100명 동시 요청에서 정확히 1건만 성공했습니다.
  2. 02Scenario B Distributed Reservation: 비관적 락 100%(50/50), Redis 분산 락 100%(50/50), 낙관적 락 40%(20/50) 성공률을 확인했습니다.
  3. 03Scenario C Mixed Load: Redis 분산 락 기준 총 1,005 RPS, 읽기 p95 7ms, 쓰기 p95 6ms를 기록했습니다.
  4. 04Redis 대기열로 100등 이내 사용자에게 토큰을 발급하고, 만료·취소 좌석은 Kafka consumer로 자동 복구했습니다.