Mastering High‑Concurrency Flash‑Sale: 7 Locking & Queue Strategies in SpringBoot
This article analyzes a high‑concurrency flash‑sale scenario using SpringBoot, MySQL, and JMeter, demonstrates seven implementations—from service‑level locks to AOP, pessimistic/optimistic locks, and queue‑based designs—examines their trade‑offs with concrete code, test results, and practical recommendations.
High‑concurrency situations are common in internet companies; this article simulates such a scenario with a flash‑sale (seckill) of a product to explore various concurrency‑control techniques.
Test Environment
SpringBoot 2.5.7, MySQL 8.0, MybatisPlus, Swagger 2.9.2
Load‑testing tool: JMeter
Scenario: Decrease stock → Create order → Simulate payment
1. Initial Implementation – Service‑Level Lock
The controller calls secondKillService.startSecondKillByLock. The service method is annotated with @Transactional and surrounds the business logic with a ReentrantLock. The code checks stock, decrements it, creates an order, and inserts a payment record.
Although the lock seems correct, testing 1,000 concurrent requests for 100 items shows overselling because the lock is released in the finally block before the transaction commits, allowing other threads to read stale stock.
2. Solving Oversell – Adjusting Lock Timing
The core problem is that the lock is released before the transaction finishes. Two main ways to fix it are moving the lock to the controller layer or applying it via AOP before the transactional method executes.
2.1 Approach 1 – Lock in Controller
@PostMapping("/start/lock")
public Result startLock(long skgId){
lock.lock();
try{
// call service
}finally{
lock.unlock();
}
return Result.ok();
}Pressure tests with different concurrency/stock ratios (1000/100, 1000/1000, 2000/1000) show that when requests exceed stock, no under‑sale occurs, but when requests are less than or equal to stock, occasional under‑sale appears, which is expected.
2.2 Approach 2 – AOP Lock
A custom annotation @ServiceLock is defined and an aspect LockAspect intercepts methods annotated with it, acquiring a ReentrantLock before proceeding and releasing it afterward.
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ServiceLock {}
@Aspect
public class LockAspect{
private static final Lock lock = new ReentrantLock(true);
@Pointcut("@annotation(com.scorpios.secondkill.aop.ServiceLock)")
public void lockAspect(){}
@Around("lockAspect()")
public Object around(ProceedingJoinPoint jp) throws Throwable{
lock.lock();
try{ return jp.proceed(); }
finally{ lock.unlock(); }
}
}The service method is then annotated with @ServiceLock and retains the original transactional logic. This keeps the lock active for the whole transaction, eliminating oversell.
2.3 Approach 3 – Pessimistic Lock (FOR UPDATE)
The DAO method uses SELECT * FROM seckill WHERE seckill_id=#{skgId} FOR UPDATE to acquire a row‑level lock. The service method runs inside a transaction, reads the locked row, updates stock, creates order and payment.
2.4 Approach 4 – Pessimistic Lock via UPDATE
An
UPDATE seckill SET number=number-1 WHERE seckill_id=#{skgId} AND number>0statement directly decrements stock while holding a table lock. After a successful update, order and payment records are inserted.
2.5 Approach 5 – Optimistic Lock
A version column is added. The update statement checks the version and increments it atomically:
UPDATE seckill SET number=number-#{number}, version=version+1
WHERE seckill_id=#{skgId} AND version=#{version}If the update count is zero, the operation fails. Tests show many update‑conflict exceptions and under‑sale when request count is close to stock, so this method is not recommended.
2.6 Approach 6 – Blocking Queue
A singleton SecondKillQueue (capacity 100) stores SuccessKilled objects. Producer threads enqueue requests; a consumer thread (implemented via ApplicationRunner) dequeues and calls the AOP‑locked service method. Important notes:
Both startSecondKillByAop and startSecondKillByLock produce identical results when called from the consumer.
If queue length equals product quantity, under‑sale may appear; increasing the queue size mitigates this.
2.7 Approach 7 – Disruptor Queue
Disruptor provides a high‑performance ring‑buffer. An event factory creates SecondKillEvent objects, a translator publishes them, and a consumer handler invokes the AOP‑locked service method. Performance tests show ~600 k orders/second on a single thread, but oversell still occurs due to the time gap between enqueue and dequeue.
Note: In both the business layer and AOP method, do not throw exceptions (e.g., throw new RuntimeException() ) because an uncaught exception will terminate the consumer thread.
3. Summary of Findings
Approaches 1 and 2 (service‑level lock and controller‑level lock) solve the oversell problem by ensuring the lock lives longer than the transaction.
Approaches 3, 4, and 5 rely on database locks; the pessimistic row‑lock (FOR UPDATE) and table‑lock (UPDATE) work well, while the optimistic‑lock version performs the worst.
Approaches 6 and 7 (queue‑based) decouple request intake from processing; they require careful exception handling and may cause under‑sale when the queue length matches stock.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
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.
