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.
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
OKSample 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
Full-Stack Internet Architecture
Introducing full-stack Internet architecture technologies centered on Java
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.