Backend Development 14 min read

Designing a High‑Concurrency Flash‑Sale (秒杀) System: From Naïve Implementation to Optimized Solutions

This article walks through the design of a flash‑sale system, starting with a simple SpringBoot‑MyBatis implementation, then addressing overselling with pessimistic and optimistic locks, applying rate‑limiting algorithms, time‑window controls, interface hiding, frequency limits, and a suite of production‑grade optimizations such as CDN, Nginx load balancing, Redis caching, message queues, and short‑URL handling.

Full-Stack Internet Architecture
Full-Stack Internet Architecture
Full-Stack Internet Architecture
Designing a High‑Concurrency Flash‑Sale (秒杀) System: From Naïve Implementation to Optimized Solutions

Flash‑sale scenarios

Typical flash‑sale use cases include train ticket booking on 12306, purchasing expensive liquor, concert tickets, and large‑scale events like Double‑Eleven.

Key concerns

Strictly prevent overselling : selling more items than inventory is the core problem.

Prevent abuse : block malicious bots and scalpers.

Ensure user experience : support high QPS while keeping the UI responsive.

We will refine the design according to these concerns.

1. First version – Naïve implementation

Using SpringBoot + MyBatis the flow is:

Controller receives the request and calls the Service layer.

Service checks if sold quantity equals inventory; if not, it increments the sold count via DAO.

DAO updates the database (MyBatis) to increase sold count and create the order.

Testing with Postman works, but under real concurrent load (e.g., JMeter) the order count exceeds inventory because multiple threads read the same stock value before it is updated.

Example of the race condition:

User A reads stock = 64, updates to 63 and creates an order.

User B reads the same stock = 64, also updates to 63 and creates another order.

Result: inventory decreased by 1 but two orders were created (overselling).

2. Second version – Pessimistic lock

Apply synchronized (or database row lock) on the Controller‑Service call to serialize access.

Pros: eliminates overselling.

Cons: under high concurrency only one thread obtains the lock, causing massive contention.

@Transactional
@Service
@Transactional
@Slf4j
public class OrderServiceImpl implements OrderService {
    // check stock
    Stock stock = checkStock(id);
    // update stock
    updateSale(stock);
    // create order
    return createOrder(stock);
}

When using Spring's @Transactional for a pessimistic lock, remember:

Place MySQL statements as late as possible because the transaction begins with the first statement.

Set an appropriate transaction timeout; default is unlimited.

Note: threads that cannot acquire the lock will block, which may degrade user experience.

3. Third version – Optimistic lock

Introduce a version column for each stock record. The Service reads the current sold count and version, then updates both atomically:

UPDATE stock_table SET sold = sold + 1, version = version + 1
WHERE id = #{id} AND version = #{version}

If the update succeeds, the purchase is successful; otherwise it fails, preventing overselling while allowing higher concurrency.

4. Fourth version – Rate limiting

After solving overselling, protect the system with rate limiting. Common algorithms:

Leaky bucket

Token bucket (used by Guava's RateLimiter )

Implementation example using Guava:

private RateLimiter rateLimiter = RateLimiter.create(20);
// block until a token is available
rateLimiter.acquire();
// try to acquire with timeout
boolean ok = rateLimiter.tryAcquire(3, TimeUnit.SECONDS);

Rate limiting, together with caching and graceful degradation, shields the backend from traffic spikes.

5. Fifth version – Detailed optimizations

Time‑window restriction : only allow purchases during a predefined period (e.g., set a Redis key with 180‑second TTL).

Hide the flash‑sale API : expose a MD5 token generated from user‑id and product‑id; the real purchase URL requires this token.

Frequency limiting per user : store request count in Redis with expiration and reject when the threshold is exceeded.

Sample code for time‑window check:

127.0.0.1:6379> set kill1 1 EX 180
OK

Sample code for generating the MD5 token:

// generate MD5 token
public String getMd5(Integer id, Integer userid) {
    // validate user and product
    String hashKey = "KEY_" + userid + "_" + id;
    String key = DigestUtils.md5DigestAsHex((userid + id + "!AW#").getBytes());
    stringRedisTemplate.opsForValue().set(hashKey, key, 3600, TimeUnit.SECONDS);
    return key;
}

6. Sixth version – Additional production‑grade tweaks

CDN acceleration : serve static assets from edge locations.

Button gray‑out : disable the purchase button until the sale starts and enforce per‑second click limits.

Nginx load balancing : distribute traffic across multiple Tomcat instances to handle tens of thousands of QPS.

Redis for data storage : move hot data to Redis; use Lua scripts for atomic operations.

Message‑queue buffering : after a successful flash‑sale, push order info to RabbitMQ/Kafka for asynchronous persistence, providing a “order queued” response to the user.

Short‑URL mapping : hide the real purchase URL behind a short link.

Industrial‑grade stack : combine MQ, SpringBoot, Redis, Dubbo, Zookeeper, Maven, Lua, etc., to build a robust system.

7. References

Bilibili video: https://b23.tv/IsifGk

GitHub repository: https://github.com/qiurunze123/miaosha

BackendConcurrencyRedisoptimistic lockSpringBootRate Limitingpessimistic lockflash sale
Full-Stack Internet Architecture
Written by

Full-Stack Internet Architecture

Introducing full-stack Internet architecture technologies centered on Java

0 followers
Reader feedback

How this landed with the community

login 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.