Why Ticket Sales Fail: A Deep Dive into Concurrency, Locks, and Building a Reliable Ticket‑Booking System

When half a million users simultaneously try to buy 5,000 concert tickets, naive ordering logic leads to race conditions and double bookings, so the article walks through the root causes, compares pessimistic, optimistic, and Redis distributed locks, and presents an industrial‑grade microservice design with a three‑stage state machine to ensure strong consistency and high availability.

LuTiao Programming
LuTiao Programming
LuTiao Programming
Why Ticket Sales Fail: A Deep Dive into Concurrency, Locks, and Building a Reliable Ticket‑Booking System

Problem Modeling: Ticketing as Resource Competition

When 500,000 users simultaneously click “Buy Ticket” for 5,000 seats, the core issue is a race condition: multiple requests can read the same seat as AVAILABLE and both attempt to book it.

Classic Race‑Condition Timeline

T1: User A queries seat A1 → AVAILABLE
T2: User B queries seat A1 → STILL AVAILABLE
T3: User A updates A1 → BOOKED
T4: User B updates A1 → BOOKED

The database ends with a single record, but both users believe they have purchased the seat.

Database Design

Two tables store event and seat information. The seats table is the single source of truth.

CREATE TABLE events (
  event_id BIGINT PRIMARY KEY,
  event_name VARCHAR(255),
  event_date DATETIME,
  venue_name VARCHAR(255),
  total_seats INT,
  available_seats INT,
  status ENUM('UPCOMING','ON_SALE','SOLD_OUT'),
  sale_start_time DATETIME,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE seats (
  seat_id BIGINT PRIMARY KEY,
  event_id BIGINT,
  seat_number VARCHAR(20),
  status ENUM('AVAILABLE','RESERVED','BOOKED'),
  version BIGINT DEFAULT 0,   -- optimistic‑lock version
  reserved_by VARCHAR(50),
  reserved_until DATETIME,
  UNIQUE KEY uk_event_seat (event_id, seat_number)
);

Concurrency Control Options

Pessimistic Lock (DB Row Lock)

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select s from Seat s where s.eventId=:eventId and s.seatNumber=:seat")
Seat lockSeat(Long eventId, String seat);

Pros: simple, provides strong consistency.

Cons: blocks threads, can deadlock, fails with sharding or multiple DB instances; suitable only for single‑DB low‑concurrency.

Optimistic Lock (Version)

@Modifying
@Query("""
  update Seat s set
    s.status='RESERVED',
    s.version=s.version+1
  where s.seatId=:seatId
    and s.version=:version
    and s.status='AVAILABLE'
""")
int tryReserve(Long seatId, Long version);

Pros: non‑blocking, good performance under moderate load.

Cons: under high concurrency many retries occur; flash‑sale scenarios can cause an avalanche of failures.

Redis Distributed Lock

A flash‑sale system is a distributed resource‑competition problem; a cross‑node lock is required.

Basic Redis Lock (Correct Usage)

String lockKey = "seat:lock:" + eventId + ":" + seatNumber;
String lockValue = UUID.randomUUID().toString();
Boolean locked = redisTemplate.opsForValue()
    .setIfAbsent(lockKey, lockValue, Duration.ofSeconds(30));
if (!locked) {
    throw new SeatNotAvailableException();
}

Unlock with Lua Script

if redis.call('get', KEYS[1]) == ARGV[1] then
  return redis.call('del', KEYS[1])
else
  return 0
end

The script guarantees that only the lock owner can delete the key, preventing a scenario where a lock expires, another node acquires it, and the first node later deletes the new lock.

Industrial‑Grade Ticket Flow (Three‑Stage State Machine)

AVAILABLE → RESERVED (10 minutes) → BOOKED

Step 1: Batch Lock Seats (prevent deadlock)

seatNumbers.stream()
    .sorted() // enforce a global order
    .forEach(this::lockSeat);

Step 2: Create Temporary Reservation

seat.setStatus("RESERVED");
seat.setReservedUntil(now.plusMinutes(10));

Step 3: Payment Success → Confirm Order

seat.setStatus("BOOKED");
bookingRepository.save(booking);

Step 4: Expiration Recovery (Scheduled Task)

@Scheduled(fixedRate = 60000)
public void releaseExpired() {
    // turn expired RESERVED seats back to AVAILABLE
}

System Decomposition (Microservice Boundaries)

Event Service : manages event information.

Inventory Service : handles seat data and distributed locks.

Booking Service : processes orders.

Payment Adapter : isolates payment interactions.

Search Service : provides search capabilities (e.g., Elasticsearch).

Principle: services that require strong consistency must be isolated; high‑read services can be asynchronous.

Trade‑off Comparison of Lock Solutions

DB Pessimistic Lock : blocks, not distributed, not recommended.

Optimistic Lock : non‑blocking, not distributed, suitable for medium‑low concurrency.

Redis Lock : non‑blocking, distributed, mainstream choice.

Redlock : non‑blocking, distributed, provides ultra‑high availability.

Conclusion

Stability, not raw speed, determines success in a ticket‑booking system. Essential components are a distributed lock, a clear seat state machine, timeout recovery, idempotent/compensating actions, and well‑defined service boundaries. This design satisfies both interview preparation for high‑concurrency scenarios and production‑grade deployments.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

concurrencyRedisDistributed Lockoptimistic lockpessimistic lockticketing system
LuTiao Programming
Written by

LuTiao Programming

LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.