Solving Product Overselling in High‑Concurrency Scenarios: Seven Implementation Methods
This article analyzes the overselling problem that occurs during high‑concurrency flash‑sale (seckill) operations and presents seven concrete solutions—including improved lock placement, AOP‑based locking, three types of database locks, optimistic locking, a blocking‑queue approach, and a Disruptor queue—complete with SpringBoot 2.5.7 code samples, performance test results, and practical recommendations.
1. Introduction
High‑concurrency situations are common in internet companies. This article uses a product‑flash‑sale (seckill) scenario to simulate such conditions, providing all source code, scripts, and test cases at the end.
Environment: SpringBoot 2.5.7, MySQL 8.0, MybatisPlus, Swagger 2.9.2
Simulation tool: JMeter
Simulation flow: Reduce inventory → Create order → Simulate payment
2. Product Flash‑Sale – Overselling
Typical code adds @Transactional and a lock inside the service layer, but testing with 1,000 concurrent requests for 100 items still produces overselling because the lock is released before the transaction commits.
@ApiOperation(value="秒杀实现方式——Lock加锁")
@PostMapping("/start/lock")
public Result startLock(long skgId){
// ... lock logic ...
return Result.ok();
}The premature lock release leads to inventory being deducted multiple times.
3. Solving the Overselling Problem
The core issue is that the lock is released before the transaction finishes. The following seven methods move the lock acquisition to an earlier stage.
3.1 Method 1 – Improved Lock (Lock in Controller)
@PostMapping("/start/lock")
public Result startLock(long skgId){
lock.lock(); // lock before business logic
try{
// business logic
}finally{
lock.unlock(); // release after transaction
}
return Result.ok();
}Testing with various concurrency‑product ratios shows that when the request count exceeds the product count, no under‑selling occurs, but when they are comparable, occasional under‑selling may appear.
3.2 Method 2 – AOP‑Based Lock
Define a custom annotation @ServiceLock and an aspect that locks before the annotated method executes.
@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.example.ServiceLock)")
public void lockAspect(){}
@Around("lockAspect()")
public Object around(ProceedingJoinPoint p) throws Throwable {
lock.lock();
try { return p.proceed(); }
finally { lock.unlock(); }
}
}This keeps the locking logic separate from business code, making it more elegant.
3.3 Method 3 – Pessimistic Lock (FOR UPDATE)
@Select("SELECT * FROM seckill WHERE seckill_id=#{skgId} FOR UPDATE")
SecondKill querySecondKillForUpdate(@Param("skgId") Long skgId);Row‑level lock is held until the transaction commits.
3.4 Method 4 – Pessimistic Lock (UPDATE Statement)
@Update("UPDATE seckill SET number=number-1 WHERE seckill_id=#{skgId} AND number>0")
int updateSecondKillById(@Param("skgId") long skgId);Directly updates the row, acquiring a table lock for the duration of the statement.
3.5 Method 5 – Optimistic Lock
Uses a version field 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);High contention leads to many update failures; not recommended for large‑scale seckill.
3.6 Method 6 – Blocking Queue
Requests are placed into a fixed‑size LinkedBlockingQueue ; a consumer thread processes them sequentially.
public class SecondKillQueue {
static final int QUEUE_MAX_SIZE = 100;
static BlockingQueue
blockingQueue = new LinkedBlockingQueue<>(QUEUE_MAX_SIZE);
// singleton getter, produce/consume methods omitted for brevity
}Queue length must be larger than product count to avoid under‑selling.
3.7 Method 7 – Disruptor Queue
Uses LMAX Disruptor for ultra‑low‑latency event processing.
public class SecondKillEventFactory implements EventFactory
{
public SecondKillEvent newInstance() { return new SecondKillEvent(); }
}
public class SecondKillEventProducer {
private static final EventTranslatorVararg
translator =
(event, seq, args) -> { event.setSeckillId((Long)args[0]); event.setUserId((Long)args[1]); };
// publish logic omitted
}Performance improves, but overselling can still occur due to the gap between enqueue and dequeue.
4. Summary
Methods 1 and 2 solve the lock‑timing issue by acquiring the lock before the transaction starts.
Methods 3‑5 rely on database‑level locks; method 5 (optimistic) performs the worst under high contention.
Methods 6‑7 use queue‑based processing; they require careful exception handling (no uncaught RuntimeException) and a queue size larger than the product count to avoid under‑selling.
All seven approaches have been verified with three test scenarios: (1) 1,000 concurrent requests for 100 items, (2) 1,000 concurrent requests for 1,000 items, and (3) 2,000 concurrent requests for 1,000 items. Future work includes exploring distributed solutions.
Source code repository: https://github.com/Hofanking/springboot-second-skill-example
Top Architect
Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.
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.