How to Prevent Overselling in High‑Concurrency Flash Sale Systems

This article explores common overselling problems in high‑concurrency flash‑sale scenarios and presents seven practical solutions—including lock timing adjustments, AOP locking, pessimistic and optimistic database locks, and queue‑based approaches—each illustrated with SpringBoot code and performance test results.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
How to Prevent Overselling in High‑Concurrency Flash Sale Systems

High‑concurrency scenarios are common in internet companies; this article simulates such a scenario using a product flash‑sale (seckill) to demonstrate overselling issues and solutions.

Environment: SpringBoot 2.5.7, MySQL 8.0, MybatisPlus, Swagger 2.9.2

Simulation tool: JMeter

Simulation flow: Reduce stock → Create order → Simulate payment

1. Product Seckill – Overselling

Typical code adds @Transactional and a lock in the service layer, while the controller simply calls the service.

@ApiOperation(value="秒杀实现方式——Lock加锁")
@PostMapping("/start/lock")
public Result startLock(long skgId){
    // ... generate userId and call service
    Result result = secondKillService.startSecondKillByLock(skgId, userId);
    return Result.ok();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result startSecondKillByLock(long skgId, long userId){
    lock.lock();
    try {
        // check stock, deduct, create order, simulate payment
    } finally {
        lock.unlock();
    }
    return Result.ok(SecondKillStateEnum.SUCCESS);
}

Testing 1,000 concurrent requests for 100 items shows overselling because the lock is released before the transaction commits.

overselling test result
overselling test result

2. Solving Overselling

The core problem is the lock release timing; moving the lock acquisition to a point before the transaction starts resolves the issue. Several approaches are presented.

2.1 Method 1 – Improved Lock (Lock in Controller)

@PostMapping("/start/lock")
public Result startLock(long skgId){
    lock.lock(); // lock before service call
    try {
        // call service as before
    } finally {
        lock.unlock();
    }
    return Result.ok();
}

Three test scenarios are shown: 1,000 concurrent requests for 100 items, 1,000 concurrent for 1,000 items, and 2,000 concurrent for 1,000 items.

When concurrency exceeds stock, no under‑selling occurs.

When concurrency is less than or equal to stock, occasional under‑selling may appear.

2.2 Method 2 – AOP Lock

A custom annotation @ServiceLock and an aspect acquire the lock before the service method executes.

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ServiceLock { String description() default ""; }
@Aspect
@Component
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){
    // same business logic as lock method
    return Result.ok(SecondKillStateEnum.SUCCESS);
}

2.3 Method 3 – Pessimistic Lock (FOR UPDATE)

@Transactional(rollbackFor = Exception.class)
public Result startSecondKillByUpdate(long skgId, long userId){
    SecondKill secondKill = secondKillMapper.querySecondKillForUpdate(skgId);
    // deduct stock, create order, simulate payment
    return Result.ok(SecondKillStateEnum.SUCCESS);
}
@Select("SELECT * FROM seckill WHERE seckill_id=#{skgId} FOR UPDATE")
SecondKill querySecondKillForUpdate(@Param("skgId") Long skgId);

2.4 Method 4 – Pessimistic Lock (UPDATE Statement)

@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);

2.5 Method 5 – Optimistic Lock

Uses a version field to detect concurrent updates; if the update count is zero, the purchase fails.

@Transactional(rollbackFor = Exception.class)
public Result startSecondKillByPesLock(long skgId, long userId, int number){
    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);
        }
    }
    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);

2.6 Method 6 – Blocking Queue

A fixed‑size LinkedBlockingQueue stores purchase requests; 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 seckillService;
    @Override public void run(ApplicationArguments args) {
        new Thread(() -> {
            while (true) {
                try {
                    SuccessKilled kill = SecondKillQueue.getSkillQueue().consume();
                    if (kill != null) {
                        Result r = seckillService.startSecondKillByAop(kill.getSeckillId(), kill.getUserId());
                        // log success
                    }
                } catch (InterruptedException e) { e.printStackTrace(); }
            }
        }).start();
    }
}

2.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 static 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); }
}

Testing shows Disruptor improves throughput but still suffers from occasional overselling due to the queue‑to‑database gap.

3. Summary

Methods 1 & 2 solve concurrency by adjusting lock timing relative to the transaction.

Methods 3‑5 rely on database‑level locks; pessimistic locks (FOR UPDATE, UPDATE) are more reliable than optimistic version checks.

Methods 6‑7 use in‑memory queues (blocking queue, Disruptor) to serialize requests, but they can cause under‑selling if queue length differs from stock.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavadatabaseconcurrencySpringBootLockSeckill
Su San Talks Tech
Written by

Su San Talks Tech

Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.