Problem Statements
- Limited inventory: Exactly 200 tickets, no overselling.
- One ticket per user: Strictly enforce (assuming logged-in users only).
- Handle high concurrency: 20k+ users rushing at sale start, avoid server overload/high peak load.
- Minimize database stress: Avoid hammering the persistent DB during the peak.
Typical Solutions Fitting Your Node.js Setup
To meet your requirements:
- Use Redis as the primary hotspot for inventory and user checks (in-memory, distributed via clustering).
- Pre-load remaining tickets as a counter (e.g., atomic DECR).
- Use a Set for sold users (check/add atomically).
- Best: Redis Lua script (or Redis function in v7+) for atomicity: check stock >0, check user not bought, decrement stock, add user.
- Enforce one per user via user ID in a Redis Set.
- Handle concurrency: Virtual queue (e.g., Redis list or separate service like RabbitMQ/Kafka) or waiting room to throttle ingress.
- Offload DB: Successful attempts go to a message queue (e.g., Kafka/RabbitMQ) for async persistence; failures reject immediately.
- Node.js scaling: Cluster with PM2, share Redis, use worker threads for non-blocking I/O.
This pattern prevents overselling reliably while keeping peak DB hits near zero.
Proposed Solutions
1. In-Memory Ticket Counter
- Use Redis to maintain a ticket counter initialized to 200.
- Each purchase attempt decrements the counter atomically.
- If the counter reaches zero, further attempts are rejected.
- Pros: Fast, low latency.
- Cons: Requires Redis setup, potential single point of failure (mitigated with clustering).
- Example Redis command:
redis-cli
DECR ticket_counter
2. User Purchase Tracking
- Maintain a Redis Set to track users who have already purchased a ticket.
- Before processing a purchase, check if the user ID exists in the Set.
- If it exists, reject the purchase; otherwise, add the user ID to the Set.
- Pros: Efficient membership checks.
- Cons: Requires additional memory in Redis.
- Example Redis commands:
- Check membership:
redis-cli
SISMEMBER purchased_users user_id
- Add user:
redis-cli
SADD purchased_users user_id
3. Atomic Purchase Operation
- Use a Redis Lua script to combine the ticket decrement and user check/add into a single atomic
- operation.
- This ensures no overselling and enforces the one-ticket-per-user rule.
- Example Lua script:
local user_id = ARGV[1]
local ticket_key = KEYS[1]
local user_set_key = KEYS[2]
local tickets = tonumber(redis.call("GET", ticket_key) or "0")
if tickets <= 0 then
return 0 -- Sold out
end
if redis.call("SISMEMBER", user_set_key, user_id) == 1 then
return -1 -- User already purchased
end
redis.call("DECR", ticket_key)
redis.call("SADD", user_set_key, user_id)
return 1 -- Purchase successful
4. Asynchronous Order Processing
- Successful purchase attempts are sent to a message queue (e.g., RabbitMQ, Kafka) for asynchronous processing.
- A separate worker service consumes messages from the queue and persists them to the database.
- Pros: Reduces load on the database during peak times.
- Cons: Increased complexity with message queue setup.
- Example flow:
- User attempts purchase.
- If successful, send message to queue.
- Worker processes message and saves order to DB.
- Acknowledges completion.
- Handles failures/retries as needed.
- Example pseudocode for sending to queue:
messageQueue.send({
userId: user_id,
timestamp: Date.now()
});
5. Throttling and Rate Limiting
- Implement a virtual queue or waiting room to control the rate of incoming purchase requests.
- This helps prevent server overload and ensures a smoother user experience.
- Pros: Reduces peak load on the system.
- Cons: May introduce slight delays for users.
- Example implementation: – Use Redis lists to queue requests. – Process requests at a controlled rate.
6. Node.js Scaling
- Use PM2 to cluster your Node.js application across multiple CPU cores.
- Share Redis connections among instances.
- Use worker threads for CPU-intensive tasks to keep the event loop responsive.
- Pros: Improved performance and reliability.
- Cons: Increased complexity in deployment and monitoring.
- Example PM2 command:
pm2 start app.js -i max
Conclusion
By leveraging Redis for atomic operations, user tracking, and asynchronous processing, you can effectively manage a flash sale with limited tickets while minimizing database load and ensuring a smooth user experience.