Backend Development 18 min read

Solving Product Overselling in High‑Concurrency Flash‑Sale (Seckill) with Multiple Locking Strategies

This article analyzes the overselling problem that occurs during high‑concurrency flash‑sale scenarios, demonstrates seven concrete implementations—including improved lock, AOP lock, pessimistic and optimistic database locks, blocking‑queue and Disruptor‑based queues—using SpringBoot, MySQL and JMeter, and summarizes their performance characteristics and trade‑offs.

Architect's Guide
Architect's Guide
Architect's Guide
Solving Product Overselling in High‑Concurrency Flash‑Sale (Seckill) with Multiple Locking Strategies

High‑concurrency flash‑sale (seckill) is a common challenge in internet companies; this article uses a product‑seckill example to simulate the scenario, providing complete source code, scripts and test cases.

Environment: SpringBoot 2.5.7, MySQL 8.0, Mybatis‑Plus, Swagger 2.9.2; simulation tool: JMeter; workflow: reduce inventory → create order → simulate payment.

1. Improved Lock (Lock in Service)

@ApiOperation(value="秒杀实现方式——Lock加锁")
@PostMapping("/start/lock")
public Result startLock(long skgId) {
    lock.lock();
    try {
        // business logic
    } finally {
        lock.unlock();
    }
    return Result.ok();
}

The lock is acquired before the transactional method runs, ensuring the lock is held until the transaction commits, thus preventing overselling.

2. AOP Lock

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ServiceLock { String description() default ""; }

@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 jp) throws Throwable {
        lock.lock();
        try { return jp.proceed(); } finally { lock.unlock(); }
    }
}

Applying @ServiceLock on the service method moves the lock acquisition to the AOP layer, keeping the controller clean.

3. Pessimistic Lock (FOR UPDATE)

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

The row is locked until the surrounding transaction finishes, avoiding concurrent updates.

4. Pessimistic Lock via UPDATE

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

Directly decrements inventory with a conditional update, relying on the database to enforce atomicity.

5. Optimistic Lock

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

Uses a version column to detect concurrent modifications; high contention leads to many update failures, so it is not recommended for flash‑sale.

6. Blocking Queue

public class SecondKillQueue {
    static final int QUEUE_MAX_SIZE = 100;
    static BlockingQueue
blockingQueue = new LinkedBlockingQueue<>(QUEUE_MAX_SIZE);
    // singleton holder pattern omitted for brevity
    public Boolean produce(SuccessKilled kill) { return blockingQueue.offer(kill); }
    public SuccessKilled consume() throws InterruptedException { return blockingQueue.take(); }
}

Requests are enqueued and processed sequentially by a consumer thread, reducing contention but introducing a slight delay that may cause “few‑sell” when queue length equals product count.

7. Disruptor Queue

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

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

Disruptor provides a high‑performance ring buffer; the consumer invokes the same service method as the blocking‑queue consumer. It improves throughput but still suffers from overselling under extreme load.

Conclusion

Lock‑in‑service and AOP‑lock (methods 1 & 2) solve the timing issue of lock release before transaction commit.

Pessimistic database locks (methods 3 & 4) and optimistic lock (method 5) handle concurrency at the DB level, with optimistic lock performing worst.

Queue‑based solutions (methods 6 & 7) serialize requests, improving stability but may cause “few‑sell” when queue size matches inventory.

All seven implementations were stress‑tested with three concurrency‑inventory configurations (1000/100, 1000/1000, 2000/1000). Future work will explore distributed locking strategies.

JavaDatabaseConcurrencylockingSpringBootqueueseckill
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

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.