7 Proven Strategies to Prevent Overselling in High‑Concurrency Flash Sales (SpringBoot)
This article explores high‑concurrency flash‑sale scenarios, demonstrates why naïve @Transactional and lock usage can still cause overselling, and presents seven concrete implementations—including improved lock, AOP lock, two pessimistic‑lock variants, optimistic lock, a blocking queue, and a Disruptor queue—complete with SpringBoot code, JMeter testing results, and performance analysis.
1. Introduction
High‑concurrency situations are common in internet companies; this article uses a product flash‑sale (秒杀) to simulate such scenarios and provides all source code, scripts, and test cases at the end.
2. Environment
Framework: SpringBoot 2.5.7 + MySQL 8.0 X + MybatisPlus + Swagger 2.9.2
Simulation tool: JMeter
Simulation flow: Reduce inventory → Create order → Simulate payment
3. Solving Product Overselling
The problem arises because the lock is released before the transaction commits, leading to overselling. The timing of lock acquisition is therefore critical.
3.1 Method One – Improved Lock (Lock in Controller)
@ApiOperation(value="秒杀实现方式——Lock加锁")
@PostMapping("/start/lock")
public Result startLock(long skgId) {
lock.lock();
try {
log.info("开始秒杀方式一...");
final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000;
Result result = secondKillService.startSecondKillByLock(skgId, userId);
if (result != null) {
log.info("用户:{}--{}", userId, result.get("msg"));
} else {
log.info("用户:{}--{}", userId, "哎呦喂,人也太多了,请稍后!");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return Result.ok();
} @Override
@Transactional(rollbackFor = Exception.class)
public Result startSecondKillByLock(long skgId, long userId) {
lock.lock();
try {
// check stock
SecondKill secondKill = secondKillMapper.selectById(skgId);
Integer number = secondKill.getNumber();
if (number > 0) {
// deduct stock
secondKill.setNumber(number - 1);
secondKillMapper.updateById(secondKill);
// create order
SuccessKilled killed = new SuccessKilled();
killed.setSeckillId(skgId);
killed.setUserId(userId);
killed.setState((short) 0);
killed.setCreateTime(new Timestamp(System.currentTimeMillis()));
successKilledMapper.insert(killed);
// simulate payment
Payment payment = new Payment();
payment.setSeckillId(skgId);
payment.setUserId(userId);
payment.setMoney(40);
payment.setState((short) 1);
payment.setCreateTime(new Timestamp(System.currentTimeMillis()));
paymentMapper.insert(payment);
} else {
return Result.error(SecondKillStateEnum.END);
}
} catch (Exception e) {
throw new ScorpiosException("异常了个乖乖");
} finally {
lock.unlock();
}
return Result.ok(SecondKillStateEnum.SUCCESS);
}This approach locks before the transaction starts, preventing the lock from being released prematurely.
3.2 Method Two – AOP Lock
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ServiceLock {
String description() default "";
} @Slf4j
@Component
@Scope
@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) {
lock.lock();
Object obj = null;
try {
obj = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
throw new RuntimeException();
} finally {
lock.unlock();
}
return obj;
}
} @Override
@ServiceLock // use AOP lock
@Transactional(rollbackFor = Exception.class)
public Result startSecondKillByAop(long skgId, long userId) {
// same business logic as method one
// ... (stock check, deduct, order creation, payment) ...
return Result.ok(SecondKillStateEnum.SUCCESS);
} @ApiOperation(value="秒杀实现方式二——Aop加锁")
@PostMapping("/start/aop")
public Result startAop(long skgId) {
try {
log.info("开始秒杀方式二...");
final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000;
Result result = secondKillService.startSecondKillByAop(skgId, userId);
if (result != null) {
log.info("用户:{}--{}", userId, result.get("msg"));
} else {
log.info("用户:{}--{}", userId, "哎呦喂,人也太多了,请稍后!");
}
} catch (Exception e) {
e.printStackTrace();
}
return Result.ok();
}AOP makes the locking code cleaner and keeps it outside the business method.
3.3 Method Three – Pessimistic Lock (FOR UPDATE)
@ApiOperation(value="秒杀实现方式三——悲观锁")
@PostMapping("/start/pes/lock/one")
public Result startPesLockOne(long skgId) {
try {
log.info("开始秒杀方式三...");
final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000;
Result result = secondKillService.startSecondKillByUpdate(skgId, userId);
if (result != null) {
log.info("用户:{}--{}", userId, result.get("msg"));
} else {
log.info("用户:{}--{}", userId, "哎呦喂,人也太多了,请稍后!");
}
} catch (Exception e) {
e.printStackTrace();
}
return Result.ok();
} @Override
@Transactional(rollbackFor = Exception.class)
public Result startSecondKillByUpdate(long skgId, long userId) {
try {
// lock row with FOR UPDATE
SecondKill secondKill = secondKillMapper.querySecondKillForUpdate(skgId);
Integer number = secondKill.getNumber();
if (number > 0) {
// deduct stock
secondKill.setNumber(number - 1);
secondKillMapper.updateById(secondKill);
// create order and payment (same as above)
} else {
return Result.error(SecondKillStateEnum.END);
}
} catch (Exception e) {
throw new ScorpiosException("异常了个乖乖");
}
return Result.ok(SecondKillStateEnum.SUCCESS);
} @Select("SELECT * FROM seckill WHERE seckill_id=#{skgId} FOR UPDATE")
SecondKill querySecondKillForUpdate(@Param("skgId") Long skgId);3.4 Method Four – Pessimistic Lock (UPDATE Statement)
@ApiOperation(value="秒杀实现方式四——UPDATE锁表")
@PostMapping("/start/pes/lock/two")
public Result startPesLockTwo(long skgId) {
// similar controller as method three
// ...
} @Update("UPDATE seckill SET number=number-1 WHERE seckill_id=#{skgId} AND number > 0")
int updateSecondKillById(@Param("skgId") long skgId);This directly updates the stock row; if the update count is zero, the product is sold out.
3.5 Method Five – Optimistic Lock
@ApiOperation(value="秒杀实现方式五——乐观锁")
@PostMapping("/start/opt/lock")
public Result startOptLock(long skgId) {
try {
log.info("开始秒杀方式五...");
final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000;
Result result = secondKillService.startSecondKillByPesLock(skgId, userId, 1);
if (result != null) {
log.info("用户:{}--{}", userId, result.get("msg"));
} else {
log.info("用户:{}--{}", userId, "哎呦喂,人也太多了,请稍后!");
}
} catch (Exception e) {
e.printStackTrace();
}
return Result.ok();
} @Override
@Transactional(rollbackFor = Exception.class)
public Result startSecondKillByPesLock(long skgId, long userId, int number) {
try {
SecondKill kill = secondKillMapper.selectById(skgId);
if (kill.getNumber() >= number) {
int result = secondKillMapper.updateSecondKillByVersion(number, skgId, kill.getVersion());
if (result > 0) {
// create order and payment
} else {
return Result.error(SecondKillStateEnum.END);
}
} else {
return Result.error(SecondKillStateEnum.END);
}
} catch (Exception e) {
throw new ScorpiosException("异常了个乖乖");
}
return Result.ok(SecondKillStateEnum.SUCCESS);
} @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);Because many updates may fail, this method often leads to many exceptions and is not recommended for high‑traffic flash sales.
3.6 Method Six – Blocking Queue
public class SecondKillQueue {
static final int QUEUE_MAX_SIZE = 100;
static BlockingQueue<SuccessKilled> blockingQueue = new LinkedBlockingQueue<>(QUEUE_MAX_SIZE);
private SecondKillQueue() {}
private static class SingletonHolder { private static final SecondKillQueue queue = new SecondKillQueue(); }
public static SecondKillQueue getSkillQueue() { return SingletonHolder.queue; }
public Boolean produce(SuccessKilled kill) { return blockingQueue.offer(kill); }
public SuccessKilled consume() throws InterruptedException { return blockingQueue.take(); }
public int size() { return blockingQueue.size(); }
} @Component
public class TaskRunner implements ApplicationRunner {
@Autowired
private SecondKillService seckillService;
@Override
public void run(ApplicationArguments args) {
new Thread(() -> {
log.info("队列启动成功");
while (true) {
try {
SuccessKilled kill = SecondKillQueue.getSkillQueue().consume();
if (kill != null) {
Result result = seckillService.startSecondKillByAop(kill.getSeckillId(), kill.getUserId());
if (result != null && result.equals(Result.ok(SecondKillStateEnum.SUCCESS))) {
log.info("TaskRunner从消息队列取出用户,用户:{} 秒杀成功", kill.getUserId());
}
}
} catch (InterruptedException e) { e.printStackTrace(); }
}
}).start();
}
} @ApiOperation(value="秒杀实现方式六——消息队列")
@PostMapping("/start/queue")
public Result startQueue(long skgId) {
try {
log.info("开始秒杀方式六...");
final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000;
SuccessKilled kill = new SuccessKilled();
kill.setSeckillId(skgId);
kill.setUserId(userId);
Boolean flag = SecondKillQueue.getSkillQueue().produce(kill);
if (flag) {
log.info("用户:{} 秒杀成功", userId);
} else {
log.info("用户:{} 秒杀失败", userId);
}
} catch (Exception e) { e.printStackTrace(); }
return Result.ok();
}The queue decouples request reception from processing, but a time gap between enqueue and dequeue can still cause under‑selling.
3.7 Method Seven – Disruptor Queue
public class SecondKillEventFactory implements EventFactory<SecondKillEvent> {
@Override
public SecondKillEvent newInstance() { return new SecondKillEvent(); }
} public class SecondKillEvent implements Serializable {
private long seckillId;
private long userId;
// getters & setters omitted
} public class SecondKillEventProducer {
private final EventTranslatorVararg<SecondKillEvent> translator = (event, seq, args) -> {
event.setSeckillId((Long) args[0]);
event.setUserId((Long) args[1]);
};
private final RingBuffer<SecondKillEvent> ringBuffer;
public SecondKillEventProducer(RingBuffer<SecondKillEvent> ringBuffer) { this.ringBuffer = ringBuffer; }
public void secondKill(long seckillId, long userId) {
ringBuffer.publishEvent(translator, seckillId, userId);
}
} @Slf4j
public class SecondKillEventConsumer implements EventHandler<SecondKillEvent> {
private SecondKillService secondKillService = (SecondKillService) SpringUtil.getBean("secondKillService");
@Override
public void onEvent(SecondKillEvent event, long seq, boolean endOfBatch) {
Result result = secondKillService.startSecondKillByAop(event.getSeckillId(), event.getUserId());
if (result.equals(Result.ok(SecondKillStateEnum.SUCCESS))) {
log.info("用户:{} 秒杀成功", event.getUserId());
}
}
} public class DisruptorUtil {
static Disruptor<SecondKillEvent> disruptor;
static {
SecondKillEventFactory factory = new SecondKillEventFactory();
int ringBufferSize = 1024;
ThreadFactory threadFactory = Runnable::new Thread;
disruptor = new Disruptor<>(factory, ringBufferSize, threadFactory);
disruptor.handleEventsWith(new SecondKillEventConsumer());
disruptor.start();
}
public static void producer(SecondKillEvent kill) {
RingBuffer<SecondKillEvent> ringBuffer = disruptor.getRingBuffer();
new SecondKillEventProducer(ringBuffer).secondKill(kill.getSeckillId(), kill.getUserId());
}
} @ApiOperation(value="秒杀实现方式七——Disruptor队列")
@PostMapping("/start/disruptor")
public Result startDisruptor(long skgId) {
try {
log.info("开始秒杀方式七...");
final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000;
SecondKillEvent kill = new SecondKillEvent();
kill.setSeckillId(skgId);
kill.setUserId(userId);
DisruptorUtil.producer(kill);
} catch (Exception e) { e.printStackTrace(); }
return Result.ok();
}Disruptor offers higher throughput than the custom blocking queue but still suffers from occasional overselling when queue length equals product count.
4. Summary
Methods 1 and 2 solve concurrency by acquiring a lock before the transaction begins.
Methods 3, 4, and 5 rely on database‑level locks (row lock via FOR UPDATE, table lock via UPDATE, and version‑based optimistic lock). Optimistic lock performs the worst.
Methods 6 and 7 use in‑memory queues (blocking queue and Disruptor) to serialize requests; they require careful handling of exceptions to avoid consumer thread termination and may still cause under‑selling due to enqueue‑dequeue latency.
All implementations were tested under three scenarios: 1000 concurrent requests for 100 items, 1000 concurrent requests for 1000 items, and 2000 concurrent requests for 1000 items. Results show that proper lock timing eliminates overselling, while queue‑based approaches improve performance but can introduce under‑selling when queue size matches inventory.
Future work will explore distributed solutions for high‑concurrency flash sales.
Source code repository: https://github.com/Hofanking/springboot-second-skill-example
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's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
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.
