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:
  1. User attempts purchase.
  2. If successful, send message to queue.
  3. Worker processes message and saves order to DB.
  4. Acknowledges completion.
  5. Handles failures/retries as needed.
  6. 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.