Implementing High‑Concurrency SecKill (Flash Sale) in SpringBoot: Locking, Transaction, and Queue Strategies
This article demonstrates how to simulate a high‑concurrency flash‑sale scenario with SpringBoot and MySQL, analyzes why naive lock‑and‑transaction code causes overselling, and presents six refined solutions—including early locking, AOP, pessimistic and optimistic database locks, blocking queues, and Disruptor queues—along with performance observations and a concise summary.
High concurrency scenarios are common in internet companies; this article uses a flash‑sale (sec‑kill) simulation to demonstrate various solutions.
Environment: SpringBoot 2.5.7, MySQL 8.0, MybatisPlus, Swagger2.9.2; Tool: JMeter; Scenario: reduce inventory → create order → simulate payment.
1. Product SecKill – Oversell
Typical implementation adds @Transactional and a lock in the service layer, but testing 1000 concurrent requests for 100 items shows overselling because the lock is released before the transaction commits.
@ApiOperation(value="秒杀实现方式——Lock加锁")
@PostMapping("/start/lock")
public Result startLock(long skgId){
try {
log.info("开始秒杀方式一...");
final 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 {
}
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);
}2. Solving Oversell
The key is to lock before the transaction starts. Several approaches are presented:
2.1 Improved Locking (Lock in Controller)
Lock is acquired at the controller entry and released after the service call, preventing premature lock release.
@ApiOperation(value="秒杀实现方式——Lock加锁")
@PostMapping("/start/lock")
public Result startLock(long skgId){
// 在此处加锁
lock.lock();
try {
log.info("开始秒杀方式一...");
final 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();
}Tested with various concurrency/product ratios (e.g., 1000 threads vs 100 items, 1000 vs 1000, 2000 vs 1000).
2.2 AOP Locking
Define a custom @ServiceLock annotation and an aspect that locks around the service method.
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ServiceLock {
String description() default "";
}
@Slf4j
@Component
@Scope
@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) {
lock.lock();
Object obj = null;
try {
obj = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
throw new RuntimeException();
} finally {
lock.unlock();
}
return obj;
}
}Service method is annotated with @ServiceLock and retains the transactional logic.
@Override
@ServiceLock
@Transactional(rollbackFor = Exception.class)
public Result startSecondKillByAop(long skgId, long userId) {
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("异常了个乖乖");
}
return Result.ok(SecondKillStateEnum.SUCCESS);
}2.3 Pessimistic Lock – Row Lock
Use SELECT ... FOR UPDATE within a transaction to lock the row.
@Select("SELECT * FROM seckill WHERE seckill_id=#{skgId} FOR UPDATE")
SecondKill querySecondKillForUpdate(@Param("skgId") Long skgId);
@Override
@Transactional(rollbackFor = Exception.class)
public Result startSecondKillByUpdate(long skgId, long userId) {
try {
SecondKill secondKill = secondKillMapper.querySecondKillForUpdate(skgId);
Integer number = secondKill.getNumber();
if (number > 0) {
secondKill.setNumber(number - 1);
secondKillMapper.updateById(secondKill);
// create order and payment (omitted for brevity)
} else {
return Result.error(SecondKillStateEnum.END);
}
} catch (Exception e) {
throw new ScorpiosException("异常了个乖乖");
}
return Result.ok(SecondKillStateEnum.SUCCESS);
}2.4 Pessimistic Lock – Table Update
Execute an UPDATE ... WHERE number>0 statement that acquires a table lock.
@Update("UPDATE seckill SET number=number-1 WHERE seckill_id=#{skgId} AND number > 0")
int updateSecondKillById(@Param("skgId") long skgId);
@Override
@Transactional(rollbackFor = Exception.class)
public Result startSecondKillByUpdateTwo(long skgId, long userId) {
int result = secondKillMapper.updateSecondKillById(skgId);
if (result > 0) {
// create order and payment (omitted)
} else {
return Result.error(SecondKillStateEnum.END);
}
return Result.ok(SecondKillStateEnum.SUCCESS);
}2.5 Optimistic Lock
Add a version column and update with WHERE version=? ; high contention leads to many update failures.
@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);
@Override
@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 (omitted)
} else {
return Result.error(SecondKillStateEnum.END);
}
}
return Result.ok(SecondKillStateEnum.SUCCESS);
}2.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);
private SecondKillQueue() {}
private static class SingletonHolder { private static 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(); }
}
@Slf4j
@Component
public class TaskRunner implements ApplicationRunner {
@Autowired private SecondKillService seckillService;
@Override
public void run(ApplicationArguments args) {
new Thread(() -> {
log.info("队列启动成功");
while (true) {
try {
SuccessKilled kill = SecondKillQueue.getSkillQueue().consume();
if (kill != null) {
Result result = seckillService.startSecondKillByAop(kill.getSeckillId(), kill.getUserId());
if (result != null && result.equals(Result.ok(SecondKillStateEnum.SUCCESS))) {
log.info("TaskRunner,result:{}", result);
log.info("TaskRunner从消息队列取出用户,用户:{}秒杀成功", kill.getUserId());
}
}
} catch (InterruptedException e) { e.printStackTrace(); }
}
}).start();
}
}
@ApiOperation(value="秒杀实现方式六——消息队列")
@PostMapping("/start/queue")
public Result startQueue(long skgId){
try {
log.info("开始秒杀方式六...");
final 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);
if (flag) {
log.info("用户:{}秒杀成功", kill.getUserId());
} else {
log.info("用户:{}秒杀失败", userId);
}
} catch (Exception e) { e.printStackTrace(); }
return Result.ok();
}2.7 Disruptor Queue
Leverages the LMAX Disruptor for high‑performance event processing; similar oversell issues appear but with higher throughput.
public class SecondKillEventFactory implements EventFactory
{
@Override public SecondKillEvent newInstance() { return new SecondKillEvent(); }
}
public class SecondKillEvent implements Serializable {
private long seckillId; private long userId; /* getters/setters omitted */
}
public class SecondKillEventProducer {
private final static 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 seq, boolean end) {
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 = Runnable::new;
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());
}
}
@ApiOperation(value="秒杀实现方式七——Disruptor队列")
@PostMapping("/start/disruptor")
public Result startDisruptor(long skgId){
try {
log.info("开始秒杀方式七...");
final long userId = (int) (new Random().nextDouble() * (99999 - 10000 + 1)) + 10000;
SecondKillEvent kill = new SecondKillEvent();
kill.setSeckillId(skgId);
kill.setUserId(userId);
DisruptorUtil.producer(kill);
} catch (Exception e) { e.printStackTrace(); }
return Result.ok();
}3. Summary
Methods 1‑2 solve oversell by moving lock acquisition before the transaction commits; methods 3‑5 rely on database locks with varying effectiveness (row lock, table lock, optimistic version check); methods 6‑7 use in‑process queues to avoid lock contention but may cause under‑selling due to queue latency.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
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.