Backend Development 20 min read

Resolving Overselling in High‑Concurrency Flash Sale: Seven Locking and Queue Strategies in SpringBoot

This article analyzes why simple @Transactional and lock annotations still cause overselling in flash‑sale scenarios, then presents seven backend solutions—including improved controller locking, AOP locking, pessimistic and optimistic database locks, and queue‑based approaches with BlockingQueue and Disruptor—along with code samples and JMeter test results.

Top Architect
Top Architect
Top Architect
Resolving Overselling in High‑Concurrency Flash Sale: Seven Locking and Queue Strategies in SpringBoot

In high‑concurrency scenarios such as flash‑sale (秒杀) the article demonstrates how naive @Transactional + lock implementations still cause overselling, and proposes seven solutions using SpringBoot 2.5.7, MySQL 8.0, MyBatis‑Plus.

Environment : SpringBoot 2.5.7, MySQL 8.0, MyBatis‑Plus, Swagger2.9.2, JMeter for load testing.

Method 1 – Improved lock in controller : lock before service call, release after transaction, with sample controller and service code.

@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();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result startSecondKillByLock(long skgId, long userId) {
    lock.lock();
    try {
        // 校验库存
        SecondKill secondKill = secondKillMapper.selectById(skgId);
        Integer number = secondKill.getNumber();
        if (number > 0) {
            // 扣库存
            secondKill.setNumber(number - 1);
            secondKillMapper.updateById(secondKill);
            // 创建订单
            SuccessKilled killed = new SuccessKilled();
            killed.setSeckillId(skgId);
            killed.setUserId(userId);
            killed.setState((short)0);
            killed.setCreateTime(new Timestamp(System.currentTimeMillis()));
            successKilledMapper.insert(killed);
            // 模拟支付
            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);
}

Method 2 – AOP lock : custom @ServiceLock annotation and aspect that acquires a ReentrantLock before the business method.

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ServiceLock {
    String description() default "";
}
@Component
@Aspect
@Order(1)
public class LockAspect {
    private static 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();
        Object obj = null;
        try {
            obj = joinPoint.proceed();
        } finally {
            lock.unlock();
        }
        return obj;
    }
}

Method 3 – Pessimistic lock (FOR UPDATE) : use SELECT … FOR UPDATE within a transaction.

@Select("SELECT * FROM seckill WHERE seckill_id=#{skgId} FOR UPDATE")
SecondKill querySecondKillForUpdate(@Param("skgId") Long skgId);

Method 4 – Pessimistic lock via UPDATE : update statement with condition number>0.

@Update("UPDATE seckill SET number=number-1 WHERE seckill_id=#{skgId} AND number > 0")
int updateSecondKillById(@Param("skgId") long skgId);

Method 5 – Optimistic lock : version field check and atomic update.

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

Method 6 – Blocking queue : requests are enqueued in a LinkedBlockingQueue and processed by a consumer thread.

public class SecondKillQueue {
    static final int QUEUE_MAX_SIZE = 100;
    static BlockingQueue
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(); }
}

Method 7 – Disruptor queue : high‑performance LMAX Disruptor with event factory, producer, and consumer.

public class SecondKillEventFactory implements EventFactory
{
    @Override
    public SecondKillEvent newInstance() { return new SecondKillEvent(); }
}

public class SecondKillEvent {
    private long seckillId;
    private long userId;
    // getters & setters omitted
}

public class SecondKillEventProducer {
    private static final EventTranslatorVararg
translator = (event, seq, args) -> {
        event.setSeckillId((Long) args[0]);
        event.setUserId((Long) args[1]);
    };
    private final RingBuffer
ringBuffer;
    public SecondKillEventProducer(RingBuffer
ringBuffer) { this.ringBuffer = ringBuffer; }
    public void secondKill(long seckillId, long userId) {
        ringBuffer.publishEvent(translator, seckillId, userId);
    }
}

public class SecondKillEventConsumer implements EventHandler
{
    private SecondKillService secondKillService = (SecondKillService) SpringUtil.getBean("secondKillService");
    @Override
    public void onEvent(SecondKillEvent event, long sequence, 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
disruptor;
    static {
        SecondKillEventFactory factory = new SecondKillEventFactory();
        int ringBufferSize = 1024;
        ThreadFactory threadFactory = r -> new Thread(r);
        disruptor = new Disruptor<>(factory, ringBufferSize, threadFactory);
        disruptor.handleEventsWith(new SecondKillEventConsumer());
        disruptor.start();
    }
    public static void producer(SecondKillEvent kill) {
        RingBuffer
ringBuffer = disruptor.getRingBuffer();
        new SecondKillEventProducer(ringBuffer).secondKill(kill.getSeckillId(), kill.getUserId());
    }
}

Testing with JMeter under three load scenarios (1000×100, 1000×1000, 2000×1000) shows which methods avoid overselling and which may cause under‑selling.

Conclusion : locking before transaction (methods 1‑2) solves overselling; database locks (methods 3‑5) have varying trade‑offs; queue‑based approaches (methods 6‑7) prevent race conditions but can lead to under‑selling due to enqueue‑dequeue latency, and must avoid throwing exceptions that would stop consumer threads.

concurrencyDistributedSystemsMySQLJMeterlockingSpringBootQueue
Top Architect
Written by

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.

0 followers
Reader feedback

How this landed with the community

login 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.