Designing a Million-User Flash Sale System: Lessons from a Spring Festival Red‑Envelope Project

This article walks through the core contradictions of flash‑sale systems, presents a layered architecture using CDN, Nginx, Redis, MQ and a database, explains key implementation details and common pitfalls, and offers incremental optimization steps for building a robust, high‑concurrency service.

Coder Trainee
Coder Trainee
Coder Trainee
Designing a Million-User Flash Sale System: Lessons from a Spring Festival Red‑Envelope Project

Hello, I'm Yu Wan. Many friends ask how to answer an interview question that asks you to design a flash‑sale (秒杀) system. I recently rebuilt a flash‑sale system for a Spring Festival red‑envelope project that handled peak traffic, and I’ll share the core ideas.

1. What is the essence of a flash‑sale?

The fundamental conflict is limited inventory vs massive requests . For example, 100 items and 100,000 users – only 100 can succeed, the rest are “spectators”. The design goals are:

Protect the system from crashing (prevent 100,000 users from overloading the servers).

Ensure data consistency (avoid overselling or underselling).

The philosophy is: under the premise of correctness, filter out as many invalid requests as possible .

2. What does a classic flash‑sale system look like?

Client → CDN → Reverse Proxy (Nginx) → Web Application → Redis → MQ → Database

Each layer has its own responsibility.

Layer 1: Traffic entry – CDN and Nginx

Static assets (HTML, CSS, JS, images) are placed on CDN so they never hit the application servers.

Rate limiting : limit_req_zone limits each IP to, for example, 5 requests per second.

Load balancing : distributes requests evenly across backend web servers.

Layer 2: Web application – the real filter

The core logic lives here. Direct DB access would die under load. The rule is: if it can be done in memory, never touch disk.

Key operation: Redis pre‑decrement inventory

Long remaining = redisTemplate.opsForValue().decrement("seckill:stock:1001");
if (remaining < 0) {
    // 库存不足,直接返回“已售罄”
    return "已售罄";
}
DECR

is atomic, preventing oversell and filtering out about 90% of requests.

Small optimization : after stock is exhausted, set a flag in Redis so subsequent DECR calls are skipped.

Layer 3: Message queue – asynchronous peak‑shaving

After Redis filtering, the remaining “potentially successful” requests are not written to the DB immediately; they are sent to a MQ (RocketMQ/Kafka). The web app returns “in queue, please wait”.

Peak shaving : smooths sudden traffic spikes.

Decoupling : the order system can fail without affecting the flash‑sale service.

Layer 4: Database – final consistency

Consumers pull messages from MQ, write orders to the DB, and deduct inventory as a safety net.

Database update uses an optimistic lock:

UPDATE product SET stock = stock - 1 WHERE id = 1001 AND stock > 0;

The affected‑row count indicates success (1) or that inventory has already been taken (0).

3. Three common pitfalls

Pitfall 1: Oversell

Symptom: sold 100 items but inventory shows –5.

Cause: concurrent DB updates without proper locking (e.g., missing SELECT ... FOR UPDATE).

Solution: Redis pre‑decrement + DB optimistic lock provides double protection.

Pitfall 2: Duplicate orders

Symptom: the same user succeeds twice.

Cause: rapid clicks or client retries let multiple requests pass Redis validation.

Solution: enforce per‑user purchase limits in Redis:

// 抢购前检查
String userKey = "seckill:user:1001:" + userId;
if (redisTemplate.hasKey(userKey)) {
    return "您已参与过";
}
// DECR成功后,设置用户标记,过期时间稍长
redisTemplate.opsForValue().set(userKey, "1", 30, TimeUnit.MINUTES);

Pitfall 3: Page white‑screen or slow response

Symptom: page fails to load at the start of the event.

Cause: Tomcat thread pool exhausted, often because Redis connection pool is full.

Solutions:

Deploy the flash‑sale interface on dedicated servers, separate from normal product pages.

Configure reasonable thread pools and time‑outs; fast‑fail is better than long waits.

Use CompletableFuture for asynchronous processing to avoid blocking the main thread.

4. An extreme optimization: static page + disabled button

Before the event, the flash‑sale page is fully static on CDN and the “Buy Now” button is greyed out. The start time is hard‑coded in JS. When the moment arrives, JS enables the button, allowing users to click.

This prevents any pre‑emptive requests, blocking 99% of script‑based attacks. Advanced attackers can still tamper with parameters, so backend validation remains necessary.

5. Full process recap

User clicks “Buy”.

Nginx rate‑limits; excess requests receive “Too frequent”.

Web layer checks if the user has already bought (Redis).

Web layer executes DECR in Redis to reduce stock.

If DECR succeeds, send an MQ message and immediately return “Purchase successful” or “In queue”.

MQ consumer asynchronously writes the order to the database.

User polls for order status and eventually receives the order number.

Only step 4 touches Redis (very fast) and step 5 writes to MQ (very fast). Database work is asynchronous, so the flash‑sale API typically responds within 10 ms and can handle tens of thousands of requests per second.

6. Final thoughts

The flash‑sale system is not magical; its core is layer‑by‑layer filtering : CDN filters static traffic, Nginx filters IPs, Redis filters inventory, MQ smooths peaks, and the database provides a safety net.

Recommendations for hands‑on practice:

Build the simplest version first: Redis DECR + DB write.

Run a JMeter load test to see at what concurrency the system breaks.

Gradually introduce Nginx rate‑limiting and MQ asynchronous processing, comparing benchmark results after each step.

Practice coding the whole flow beats reading the article ten times.

Question for discussion: If the flash‑sale item is a virtual coupon with massive inventory, how would you adjust this architecture?

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.

JavaRedishigh concurrencyMessage QueueNginxflash sale
Coder Trainee
Written by

Coder Trainee

Experienced in Java and Python, we share and learn together. For submissions or collaborations, DM us.

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.