🤔 I’m Curious
As someone from the provinces who moved to Seoul,
I started wondering how the holiday reservation war that repeats every Lunar New Year and Chuseok
actually works internally,
so I organized this design.
UI Flow Explanation
- First, click [Holiday Train Reservation] right at 7 AM
- Then you enter the queue and can see how many people are waiting ahead of you
- When your queue number decreases and you get access, you enter a screen where you can search for trains and select seats
- Then you can reserve a seat
- At this time, payment is not made immediately but is available on a set date later!
So let me imagine and explain how it’s designed!!
1️⃣ Reservation Start – Entering the Queue
At 7 AM, the moment a user clicks the [Holiday Reservation] button
- Client → Redis ZSET-based queue registration
ZADD waiting:queue 173820600012 user1 - key: waiting:queue
- score: timestamp (entry time)
- value: userId
public void enterQueue(String userId) {
long now = System.currentTimeMillis();
jedis.zadd("waiting:queue", now, userId);
}
📌 Kafka Event Recording At this time, simultaneously with Redis processing, produce an event to Kafka
Purpose of Use
- Logging / Auditing
- Who entered the queue and when?
- Who was issued a token?
- Asynchronous Processing
- Final DB storage
- Email / Push notifications
- External system integration
- Failure Recovery
- Queue reconstruction based on Kafka logs when Redis fails Example events: USER_ENTER_QUEUE, TOKEN_ISSUED, USER_ADMITTED
2️⃣ Real-time Queue Position Display
- Check current position by querying Redis ZSET
ZRANK waiting:queue user1 - Read-only → Fast and safe
- Time complexity: O(log N)
- Query interval: 1~3 seconds
- Alternative: WebSocket / SSE + Redis Pub/Sub
3️⃣ Admission Approval & Token Issuance
If the reservation server allows 100 people
Only the top 100 from the Redis queue are allowed entry
ZRANGE waiting:queue 0 99
⚠️ Atomic processing with Lua Script: Process queue removal + token issuance as a single transaction
if
redis.call("ZRANK", queueKey, userId) < limit
then
redis.call("ZREM", queueKey, userId)
redis.call("SET", tokenKey, userId, "EX", 300)
end
- No mid-process failure ❌
- No duplicate issuance ❌
- No race conditions ❌
At this time, why store the token in Redis after issuance?
- Storing tokens in DB causes DB bottleneck with thousands to tens of thousands of requests per second
- Client response example
{ "accessToken": "abc.def.ghi", "expiresIn": 300 }
4️⃣ Search API – Token Verification
GET /trains/search?from=Seoul&to=hometown
Authorization: Bearer {queue-access-token}
- Redis query:
GET access:token:abc123 - If exists → OK
- If not → 401 / 403
5️⃣ Seat Search & Seat Hold
Seat Search (Cache)
seat:availability:{trainId}:{date}
- Seat status: Available / Hold / Sold
Seat Hold (Lock)
SET seat:hold:{trainId}:{seatNo} userId NX EX 180
- NX: Fails if seat is already held
- EX 180: Temporary hold for 3 minutes
- Automatically released when TTL expires
Why Redis?
- Using only DB causes deadlock risk due to row lock contention
6️⃣ Payment Stage
- Proceed with payment after seat hold
- Payment involves external PG integration → The slowest section with highest failure probability
- Process after seat hold to distribute traffic
✅ Conclusion
- At reservation start time, manage user order with Redis ZSET-based queue and
record events through Kafka to secure asynchronous processing and failure recovery capability - At admission approval stage, use Lua Script to atomically process queue removal and token issuance
- Issued Queue Access Token is stored in Redis with TTL-based expiration
- Reservation APIs verify access rights through Redis query for every request
- Seat search uses Redis cache to reduce DB load and
solve concurrency issues with NX + TTL-based Redis lock when holding seats - Payment proceeds after seat hold → Traffic distribution
- Configure a reservation system that operates stably even in super-high-traffic environments like holidays by separating Redis roles into queue, cache, and lock