Implementing High‑Concurrency Flash‑Sale (Seckill) in SpringBoot: Locking Strategies, Queue Solutions, and Performance Testing
This article demonstrates how to simulate a high‑concurrency flash‑sale scenario using SpringBoot, MySQL, Mybatis‑Plus and JMeter, analyzes the overselling problem caused by premature lock release, and presents seven solutions—including lock‑first strategies, AOP, pessimistic and optimistic locks, and queue‑based approaches—along with code samples and test results.
High‑concurrency situations are common in internet companies; this article uses a product flash‑sale (seckill) to simulate such a scenario.
Environment: SpringBoot 2.5.7, MySQL 8.0, Mybatis‑Plus, Swagger 2.9.2; Simulation tool: JMeter; Scenario: Reduce inventory → Create order → Simulate payment.
1. Flash‑sale – Overselling
The typical implementation adds @Transactional and a lock in the service layer, but testing with 1,000 concurrent requests for 100 items shows overselling because the lock is released before the transaction commits.
@ApiOperation(value="秒杀实现方式——Lock加锁")
@PostMapping("/start/lock")
public Result startLock(long skgId){
lock.lock();
try{
// business logic
}finally{
lock.unlock();
}
return Result.ok();
}2. Solving Overselling
The root cause is the lock being released before the transaction finishes. The lock must be acquired earlier, e.g., in the controller or via AOP.
2.1 Method 1 – Lock in Controller (Improved)
@ApiOperation(value="秒杀实现方式——Lock加锁")
@PostMapping("/start/lock")
public Result startLock(long skgId){
lock.lock(); // lock before transaction
try{
// call service
}finally{
lock.unlock();
}
return Result.ok();
}This approach eliminates the premature unlock problem. Three pressure‑test configurations are shown: 1000 threads × 100 items, 1000 threads × 1000 items, and 2000 threads × 1000 items.
2.2 Method 2 – AOP Lock
A custom annotation @ServiceLock is defined and an aspect acquires the lock before the target method executes.
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ServiceLock { String description() default ""; } @Aspect
@Order(1)
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 joinPoint) throws Throwable {
lock.lock();
try{ return joinPoint.proceed(); }
finally{ lock.unlock(); }
}
}The service method is annotated with @ServiceLock and still uses @Transactional .
2.3 Method 3 – Pessimistic Lock (FOR UPDATE)
Using a row‑level lock in the SQL query:
@Select("SELECT * FROM seckill WHERE seckill_id=#{skgId} FOR UPDATE")
SecondKill querySecondKillForUpdate(@Param("skgId") Long skgId);The service method runs within a transaction, ensuring the row stays locked until commit.
2.4 Method 4 – Pessimistic Lock via UPDATE
@Update("UPDATE seckill SET number=number-1 WHERE seckill_id=#{skgId} AND number>0")
int updateSecondKillById(@Param("skgId") long skgId);This statement directly decrements inventory and acquires a table lock.
2.5 Method 5 – Optimistic Lock
A version column is used to detect concurrent updates:
@Update("UPDATE seckill SET number=number-#{number}, version=version+1 WHERE seckill_id=#{skgId} AND version=#{version}")
int updateSecondKillByVersion(@Param("number") int number, @Param("skgId") long skgId, @Param("version") int version);Frequent update conflicts cause many failures; this method is not recommended for flash‑sale.
2.6 Method 6 – Blocking Queue
A fixed‑size LinkedBlockingQueue stores SuccessKilled objects. A consumer thread continuously polls the queue and invokes the service method.
public class SecondKillQueue {
static final int QUEUE_MAX_SIZE = 100;
static BlockingQueue
blockingQueue = new LinkedBlockingQueue<>(QUEUE_MAX_SIZE);
// singleton getter, produce() and consume() methods
}The controller enqueues a request; the consumer processes it asynchronously. Queue length equal to inventory may cause under‑selling.
2.7 Method 7 – Disruptor Queue
Using LMAX Disruptor for ultra‑low‑latency event processing. An EventFactory creates SecondKillEvent objects, a translator publishes events, and a consumer calls the service method.
public class SecondKillEventFactory implements EventFactory
{
public SecondKillEvent newInstance() { return new SecondKillEvent(); }
} public class SecondKillEventConsumer implements EventHandler
{
public void onEvent(SecondKillEvent e, long seq, boolean end) {
secondKillService.startSecondKillByAop(e.getSeckillId(), e.getUserId());
}
}Testing shows Disruptor improves throughput but still suffers from occasional overselling.
3. Summary
Methods 1 & 2 solve the lock‑timing issue by acquiring the lock before the transaction starts.
Methods 3‑5 rely on database‑level locks; the pessimistic locks (FOR UPDATE, UPDATE) are reliable, while optimistic locking performs poorly.
Methods 6‑7 use queue‑based processing; they avoid direct contention but can lead to under‑selling due to the gap between enqueue and dequeue, and must avoid throwing exceptions that would stop the consumer thread.
All code samples are provided in the article; developers can choose the strategy that best fits their performance and consistency requirements.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
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.