SJH

문제 해결 포트폴리오 / 콘서트 예매 / 예약 정합성

측정 완료Concert Booking · Deep Dive 1/2

동일 좌석 오버셀링 0건 검증

100개 동시 예매 요청에서 Queue Token·좌석 락·Idempotency-Key로 success 1, fail 99를 확인했습니다.

동시 요청

100

결과

success 1 / fail 99

오버셀링

0

해결 키워드Queue Token좌석 락Idempotency-Key
프로젝트
Concert Booking
참여
개인 / BE 1
기술
Java · Spring Boot · PostgreSQL · Redis · Kafka

문제 구간 아키텍처

동일 좌석 예매 경합에서 Queue token, Reservation transaction, Outbox, Kafka, DLT, PostgreSQL 복구 기준을 연결한 문제 구간 아키텍처
동일 좌석 경합에서 Queue Token, Reservation Tx, PostgreSQL 최종 기준 데이터, Redis 보조 상태를 분리한 구조입니다.

핵심 설계 판단

  • PostgreSQL을 좌석/예약의 최종 기준 데이터로 둡니다.
  • Queue Token은 대기열 우회 요청을 제한하고, Idempotency-Key와 Seat Lock은 Reservation Tx 안에서 처리합니다.
  • Redis stock은 빠른 조회/제어용이며, 불일치는 DB 기준 reconciliation 대상으로 둡니다.

문제

  1. 동일 좌석에 여러 사용자가 동시에 접근하면 읽기-수정-쓰기 사이 race condition으로 오버셀링이 발생할 수 있었습니다.
  2. 네트워크 timeout과 client retry는 중복 예약 또는 중복 결제로 이어질 수 있었습니다.
  3. Redis 재고와 DB 예약 상태가 어긋날 때 어떤 데이터를 최종 기준 데이터로 삼아 복구할지 명확해야 했습니다.

해결

  1. Queue token을 userId + scheduleId에 바인딩해 대기열 우회 요청을 제한했습니다.
  2. Reservation transaction 안에서 Idempotency-Key 확인, 좌석 락, 예약 생성을 함께 처리했습니다.
  3. DB commit 이후 발행할 이벤트는 Outbox에 남기고 Redis stock은 PostgreSQL 기준 reconciliation 대상으로 두었습니다.

결과

  1. 동일 좌석 100개 동시 요청에서 success 1, fail 99, overselling 0을 기록했습니다.
  2. 서로 다른 좌석 50개 예약에서 pessimistic lock과 Redis distributed lock 모두 50/50 성공을 확인했습니다.
  3. 예약/결제 idempotency, race condition, DLT replay, stock reconciliation을 Testcontainers 시나리오로 검증했습니다.

검증 근거

동일 좌석 경합

측정 완료

100 concurrent requests -> success 1, fail 99, overselling 0

분산 좌석 예약

측정 완료

50명 서로 다른 좌석 예매 -> pessimistic 50/50, Redis distributed lock 50/50

Testcontainers 검증 시나리오

시나리오 검증

reservation/payment idempotency, race condition, DLT replay, stock reconciliation 검증

구현 포인트

  1. 좌석 경합은 DB transaction 안의 락과 idempotency check가 같은 경계에 있도록 정리했습니다.
  2. Redis는 빠른 조회와 queue 역할을 맡지만 좌석/예약 상태의 최종 기준 데이터로 두지 않았습니다.
  3. 이벤트 발행 의도는 Outbox에 남기되, 이 사례의 핵심 검증은 좌석 경합과 중복 예약 방지에 맞췄습니다.

세부 테스트, guard, raw artifact는 GitHub README와 docs에 정리했습니다.

GitHub 근거 보기