7 Proven Ways to Prevent Overselling in High‑Concurrency Flash Sales with Java
This article explores why simple @Transactional locking can still cause product overselling under high concurrency, then presents seven detailed Java/SpringBoot solutions—including improved locking, AOP, pessimistic and optimistic database locks, and queue‑based approaches—complete with code, performance tests and best‑practice recommendations.
1. Introduction
High‑concurrency scenarios 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.
Environment: SpringBoot 2.5.7, MySQL 8.0, MybatisPlus, Swagger 2.9.2
Simulation tool: JMeter
Simulation flow: Reduce stock → Create order → Simulate payment
2. Product Flash Sale – Overselling
Even when a service method is annotated with @Transactional and a lock is added inside the service, overselling occurs because the lock is released before the transaction commits. A test with 1,000 concurrent requests buying 100 items demonstrates the problem.
3. Solving Overselling
The key is to acquire the lock **before** the transaction starts. Seven implementation methods are described.
3.1 Method 1 – Improved Locking
Lock is taken in the controller before invoking the service and released in a finally block.
@ApiOperation(value="秒杀实现方式——Lock加锁")
@PostMapping("/start/lock")
public Result startLock(long skgId) {
lock.lock();
try {
log.info("开始秒杀方式一...");
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();
}3.2 Method 2 – AOP Locking
A custom @ServiceLock annotation and an aspect that locks around the annotated method.
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ServiceLock {
String description() default "";
}
@Slf4j
@Component
@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();
}
}
}
@ServiceLock
@Transactional(rollbackFor = Exception.class)
public Result startSecondKillByAop(long skgId, long userId) {
// business logic identical to the lock version
...
}3.3 Method 3 – Pessimistic Lock (Row Lock)
Uses SELECT ... FOR UPDATE inside a transaction to lock the row.
@Transactional(rollbackFor = Exception.class)
public Result startSecondKillByUpdate(long skgId, long userId) {
SecondKill secondKill = secondKillMapper.querySecondKillForUpdate(skgId);
int number = secondKill.getNumber();
if (number > 0) {
secondKill.setNumber(number - 1);
secondKillMapper.updateById(secondKill);
// create order and payment
...
} else {
return Result.error(SecondKillStateEnum.END);
}
return Result.ok(SecondKillStateEnum.SUCCESS);
}
@Select("SELECT * FROM seckill WHERE seckill_id=#{skgId} FOR UPDATE")
SecondKill querySecondKillForUpdate(@Param("skgId") Long skgId);3.4 Method 4 – Pessimistic Lock (Update Lock)
Executes a conditional UPDATE that decrements stock only when enough items remain.
@Transactional(rollbackFor = Exception.class)
public Result startSecondKillByUpdateTwo(long skgId, long userId) {
int result = secondKillMapper.updateSecondKillById(skgId);
if (result > 0) {
// create order and payment
...
} else {
return Result.error(SecondKillStateEnum.END);
}
return Result.ok(SecondKillStateEnum.SUCCESS);
}
@Update("UPDATE seckill SET number=number-1 WHERE seckill_id=#{skgId} AND number > 0")
int updateSecondKillById(@Param("skgId") long skgId);3.5 Method 5 – Optimistic Lock
Relies on a version column; the update succeeds only if the version matches.
@Transactional(rollbackFor = Exception.class)
public Result startSecondKillByPesLock(long skgId, long userId, int purchaseQty) {
SecondKill kill = secondKillMapper.selectById(skgId);
if (kill.getNumber() >= purchaseQty) {
int updated = secondKillMapper.updateSecondKillByVersion(purchaseQty, skgId, kill.getVersion());
if (updated > 0) {
// create order and payment
...
} else {
return Result.error(SecondKillStateEnum.END);
}
}
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);3.6 Method 6 – Blocking Queue
Requests are placed into a bounded BlockingQueue; a consumer thread processes them sequentially.
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 secondKillService;
@Override
public void run(ApplicationArguments args) {
new Thread(() -> {
while (true) {
try {
SuccessKilled kill = SecondKillQueue.getSkillQueue().consume();
if (kill != null) {
Result r = secondKillService.startSecondKillByAop(kill.getSeckillId(), kill.getUserId());
if (r.equals(Result.ok(SecondKillStateEnum.SUCCESS))) {
log.info("User:{} 秒杀成功", kill.getUserId());
}
}
} catch (InterruptedException e) { e.printStackTrace(); }
}
}).start();
}
}
@PostMapping("/start/queue")
public Result startQueue(long skgId) {
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);
log.info(flag ? "User:{} 秒杀成功" : "User:{} 秒杀失败", userId);
return Result.ok();
}3.7 Method 7 – Disruptor Queue
Uses LMAX Disruptor for ultra‑low‑latency event processing.
public class SecondKillEventFactory implements EventFactory<SecondKillEvent> {
@Override public SecondKillEvent newInstance() { return new SecondKillEvent(); }
}
public class SecondKillEventProducer {
private final RingBuffer<SecondKillEvent> ringBuffer;
public SecondKillEventProducer(RingBuffer<SecondKillEvent> ringBuffer) { this.ringBuffer = ringBuffer; }
public void secondKill(long seckillId, long userId) {
ringBuffer.publishEvent((event, seq, args) -> {
event.setSeckillId((Long) args[0]);
event.setUserId((Long) args[1]);
}, seckillId, userId);
}
}
@Slf4j
public class SecondKillEventConsumer implements EventHandler<SecondKillEvent> {
private final SecondKillService service = SpringUtil.getBean(SecondKillService.class);
@Override public void onEvent(SecondKillEvent event, long seq, boolean end) {
Result r = service.startSecondKillByAop(event.getSeckillId(), event.getUserId());
if (r.equals(Result.ok(SecondKillStateEnum.SUCCESS))) {
log.info("User:{} 秒杀成功", event.getUserId());
}
}
}
public class DisruptorUtil {
static Disruptor<SecondKillEvent> disruptor;
static { // static initializer
Disruptor<SecondKillEvent> d = new Disruptor<>(new SecondKillEventFactory(), 1024, Runnable::new);
d.handleEventsWith(new SecondKillEventConsumer());
d.start();
disruptor = d;
}
public static void producer(SecondKillEvent event) {
RingBuffer<SecondKillEvent> rb = disruptor.getRingBuffer();
new SecondKillEventProducer(rb).secondKill(event.getSeckillId(), event.getUserId());
}
}
@PostMapping("/start/disruptor")
public Result startDisruptor(long skgId) {
long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000;
SecondKillEvent ev = new SecondKillEvent();
ev.setSeckillId(skgId);
ev.setUserId(userId);
DisruptorUtil.producer(ev);
return Result.ok();
}4. Summary
Methods 1 and 2 solve the lock‑timing issue by acquiring the lock before the transaction. Methods 3‑5 rely on database‑level locks; row‑level FOR UPDATE and conditional UPDATE work well, while optimistic locking performs the worst. Methods 6‑7 use in‑memory queues to serialize requests, but they can still cause under‑selling because of the time gap between enqueue and dequeue. All seven approaches were tested under three load scenarios: 1,000 concurrent requests for 100 items, 1,000 for 1,000 items, and 2,000 for 1,000 items. Future work will explore distributed solutions.
Source code: 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.
Java High-Performance Architecture
Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.
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.
