Seven ways to achieve high concurrency spike!

  • 1 Introduction
  • 2. Commodity spike – oversold
  • 3. Solve oversold goods
  • 3.1 Method 1 (improved version locking)
  • 3.2 Method 2 (AOP version lock)
  • 3.3 Method 3 (pessimistic lock 1)
  • 3.4 Method 4 (pessimistic lock 2)
  • 3.5 Method 5 (optimistic lock)
  • 3.6 Method 6 (blocking queue)
  • 3.7. Method 7 (Disruptor queue)
  • summary

1. Introduction

High-concurrency scenarios are very common in daily work on site, especially in Internet companies. This article simulates high-concurrency scenarios by selling products in seconds. All code, scripts and test cases for the article are attached at the end of the article.

  • Environment of this article: SpringBoot 2.5.7 + MySQL 8.0 X + MybatisPlus + Swagger2.9.2
  • Simulation tool: Jmeter
  • Simulation scenario: reduce inventory -> create order -> simulate payment

2. Commodity spike – oversold

In development, you may be familiar with the following code: add @Transactional transaction annotation and Lock lock in Service

Control layer: Controller

@ApiOperation(value="Second kill implementation method - Lock and lock")
@PostMapping("/start/lock")
public Result startLock(long skgId){
   <!-- -->
    try {
   <!-- -->
        log.info("Start spike method one...");
        final long userId = (int) (new Random(). nextDouble() * (99999 - 10000 + 1)) + 10000;
        Result result = secondKillService.startSecondKillByLock(skgId, userId);
        if(result != null){
   <!-- -->
            log.info("User:{}--{}", userId, result.get("msg"));
        }else{
   <!-- -->
            log.info("User: {}--{}", userId, "Hey, there are too many people, please wait later!");
        }
    } catch (Exception e) {
   <!-- -->
        e.printStackTrace();
    } finally {
   <!-- -->

    }
    return Result.ok();
}

Business layer: Service

@Override
@Transactional(rollbackFor = Exception. class)
public Result startSecondKillByLock(long skgId, long userId) {
   <!-- -->
    lock. lock();
    try {
   <!-- -->
        // check inventory
        SecondKill secondKill = secondKillMapper. selectById(skgId);
        Integer number = secondKill. getNumber();
        if (number > 0) {
   <!-- -->
            // deduct inventory
            secondKill. setNumber(number - 1);
            secondKillMapper. updateById(secondKill);
            // Create Order
            SuccessKilled killed = new SuccessKilled();
            killed.setSeckillId(skgId);
            killed.setUserId(userId);
            killed. setState((short) 0);
            killed.setCreateTime(new Timestamp(System.currentTimeMillis()));
            successKilledMapper.insert(killed);

            // mock payment
            Payment payment = new Payment();
            payment.setSeckillId(skgId);
            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("It's abnormal, be obedient");
    } finally {
   <!-- -->
        lock. unlock();
    }
    return Result.ok(SecondKillStateEnum.SUCCESS);
}

There should be no problem with the above code, add transactions to the business method, and lock when processing the business.

But there is a problem with the above way of writing, and there will be oversold situations. Look at the test results: Simulate 1000 concurrency, grab 100 products

If Jmeter does not understand, you can refer to this article:

https://blog.csdn.net/zxd1435513775/article/details/106372446



Here, the lock is added at the beginning of the business method, and the lock is released after the end of the business method. But the transaction submission here is not like this. It is possible that the lock has been released before the transaction is submitted, which will lead to oversold products. So the timing of locking is very important!

3. Solve oversold items

For the above oversold phenomenon, the main problem occurs when the lock is released in the transaction. Before the transaction is committed, the lock has been released. (The transaction submission is executed after the entire method is executed). How to solve this problem is to advance the locking step

  • Can be locked at the controller layer
  • Aop can be used to lock before the business method is executed

3.1 Method 1 (improved version locking)

@ApiOperation(value="Second kill implementation method - Lock and lock")
@PostMapping("/start/lock")
public Result startLock(long skgId){
   <!-- -->
    // lock here
    lock. lock();
    try {
   <!-- -->
        log.info("Start spike method one...");
        final long userId = (int) (new Random(). nextDouble() * (99999 - 10000 + 1)) + 10000;
        Result result = secondKillService.startSecondKillByLock(skgId, userId);
        if(result != null){
   <!-- -->
            log.info("User:{}--{}", userId, result.get("msg"));
        }else{
   <!-- -->
            log.info("User: {}--{}", userId, "Hey, there are too many people, please wait later!");
        }
    } catch (Exception e) {
   <!-- -->
        e.printStackTrace();
    } finally {
   <!-- -->
        // release the lock here
        lock. unlock();
    }
    return Result.ok();
}

The above locking can solve the problem of lock release before the transaction is committed, and stress testing can be carried out in three situations:

  • Concurrent number 1000, commodity 100
  • Concurrent number 1000, commodity 1000
  • Concurrent number 2000, commodity 1000

For the case where the number of concurrency is greater than the number of products, the product seckill generally does not appear to be under-sold, but when the number of concurrency is less than or equal to the number of products, there may be fewer sales of products, which is also very understandable.

If there is no problem, there will be no textures, because there are many ways, and there will be too many textures

3.2 Method 2 (AOP version lock)

For the above method of locking at the control layer, it may seem inelegant, then there is another way to lock before the transaction, that is AOP

Custom AOP annotations

@Target({
   <!-- -->ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy. RUNTIME)
@Documented
public @interface ServiceLock {
   <!-- -->
    String description() default "";
}
@Slf4j
@Component
@Scope
@Aspect
@Order(1) //The smaller the order, the first to execute, but more importantly, the last to be executed first
public class LockAspect {
   <!-- -->
    /**
     * Thinking: Why not use synchronized
     * service is a singleton by default, and there is only one instance under concurrent lock
     */
    private static Lock lock = new ReentrantLock(true);