Backend Development 20 min read

Solving Product Overselling in High‑Concurrency Flash Sale Scenarios: Seven Implementation Approaches

This article analyzes the common overselling problem in high‑traffic flash‑sale systems and presents seven concrete solutions—including improved locking, AOP locking, pessimistic and optimistic locks, as well as blocking‑queue and Disruptor‑based designs—complete with SpringBoot code samples and performance testing results.

Top Architect
Top Architect
Top Architect
Solving Product Overselling in High‑Concurrency Flash Sale Scenarios: Seven Implementation Approaches

1. Introduction

High‑concurrency situations are frequent in internet companies; this article uses a product flash‑sale (seckill) scenario to simulate such conditions and provides all related code, scripts, and test cases at the end.

2. Environment

SpringBoot 2.5.7, MySQL 8.0, MybatisPlus, Swagger 2.9.2

Load testing tool: JMeter

Simulation flow: Reduce inventory → Create order → Simulate payment

3. Problem: Overselling with Simple @Transactional + Lock

The typical service method annotated with @Transactional and a lock may still cause overselling because the lock is released before the transaction commits.

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

Testing with 1,000 concurrent requests for 100 items shows overselling.

4. Solutions

4.1 Method 1 – Improved Lock (Lock before transaction)

@PostMapping("/start/lock")
public Result startLock(long skgId){
    lock.lock(); // lock before transaction
    try{
        // business logic
    }finally{
        lock.unlock();
    }
    return Result.ok();
}

Locks are held until the method finishes, preventing premature release.

4.2 Method 2 – AOP Lock

Define a custom annotation @ServiceLock and an aspect that acquires a ReentrantLock before the annotated method executes.

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

@Aspect
public class LockAspect {
    private static final Lock lock = new ReentrantLock(true);
    @Pointcut("@annotation(com.example.ServiceLock)")
    public void lockAspect() {}
    @Around("lockAspect()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        lock.lock();
        try { return pjp.proceed(); }
        finally { lock.unlock(); }
    }
}

4.3 Method 3 – Pessimistic Lock (FOR UPDATE)

Use a row‑level lock in MySQL:

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

4.4 Method 4 – Pessimistic Lock via UPDATE

Execute an UPDATE that decrements inventory only when the row exists, effectively acquiring a table lock.

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

4.5 Method 5 – Optimistic Lock

Introduce a version column and update with a version check.

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

Frequent update conflicts may cause many failures; not recommended for high contention.

4.6 Method 6 – Blocking Queue

Requests are placed into a bounded LinkedBlockingQueue ; a consumer thread processes them sequentially.

public class SecondKillQueue {
    static final int QUEUE_MAX_SIZE = 100;
    static BlockingQueue
blockingQueue = new LinkedBlockingQueue<>(QUEUE_MAX_SIZE);
    // singleton getter, produce() and consume() methods
}

4.7 Method 7 – Disruptor Queue

Leverage LMAX Disruptor for ultra‑low‑latency event processing.

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

5. Summary of Findings

Methods 1 and 2 solve the lock‑before‑transaction issue by moving the lock acquisition earlier.

Methods 3‑5 rely on database‑level locking; pessimistic locks (3,4) are reliable, optimistic lock (5) performs worst under contention.

Methods 6 and 7 use queue‑based throttling; they avoid overselling but may introduce “under‑selling” when queue length equals inventory.

All seven approaches were tested under three load scenarios: 1,000 concurrent requests for 100 items, 1,000 requests for 1,000 items, and 2,000 requests for 1,000 items. Results show varying degrees of overselling or underselling depending on the method.

6. Outlook

Future work includes exploring distributed solutions for flash‑sale concurrency control.

Source code repository: https://github.com/Hofanking/springboot-second-skill-example

distributed systemsconcurrencylockingSpringBootflash saleQueue
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.