This article only deals with oversold solutions caused by high concurrency in single applications.

Oversold is when there is a fixed number of items, say 10, but at some point a large number of concurrent requests come in, causing the item to be sold, say 100.

In this paper, 7 schemes are used to realize inventory reduction, and then the problems of each scheme are analyzed, and which scheme can solve the overselling.

Scene design

Create database:

create database mytest charset=utf8;
Copy the code

Create a list of items:

USE mytest;
DROP TABLE IF EXISTS `tb_product`;
CREATE TABLE `tb_product`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'primary key id'.`name` varchar(64) NOT NULL COMMENT 'Username, unique'.`price` decimal(10.2) NOT NULL DEFAULT 0.00 COMMENT 'price'.`stock` int(10) NOT NULL DEFAULT 0 COMMENT 'inventory',
  PRIMARY KEY (`id`) USING BTREE,
) ENGINE = InnoDB CHARACTER SET = utf8;
Copy the code

Then insert a piece of data:

INSERT INTO `mytest`.`tb_product`(`id`.`name`.`price`.`stock`) VALUES (1.'iPhone6S'.5000.00.1);
Copy the code

Now, we have one good, and its stock is 1, so there’s only one.

JMeter simulates high concurrency

JMeter can simulate high concurrency scenarios. See my article download and Use JMeter for details

The simulation comes in with 500 requests.

Option I (Services)

Let’s take a look at an item minus inventory function to analyze the problems that can occur under high concurrency:

/** * Simple inventory reduction operation, does not support high concurrency *@author cc
 * @dateThe 2021-12-30 15:04 * /
@Transactional(rollbackFor = Exception.class)
public void sampleSale(Long productId) {
    TbProduct product = productDao.selectByPrimaryKey(productId);
    if (product == null) {
        throw new RuntimeException("The item was not found.");
    }
    int stock = product.getStock() - 1;
    if (stock >= 0) {
        product.setStock(stock);
        int r = productDao.updateByPrimaryKeySelective(product);
        if (r <= 0) {
            throw new RuntimeException("Merchandise inventory reduction failed."); }}else {
        throw new RuntimeException("Out of stock"); }}Copy the code

In the above function, the information of the product is obtained first, and the inventory number is obtained. When the inventory number is enough, the inventory reduction operation is carried out.

The problem, however, is that with high concurrency, multiple threads will simultaneously read that the inventory of the item is 1 and then destock it. If there are 10 threads at a time, the destocking operation will be performed 10 times, and the number of items in stock will change from 1 to -9.

So it’s not going to work.

Scheme 2 (transaction + method lock)

Since AOP starts a transaction before a method executes, it is possible that a transaction can be started by another thread in the interval between method execution and transaction execution. A few more tries will work out * so it still won't solve the oversold problem *@author cc
 * @dateThe 2021-12-30 sets * /
@Transactional(rollbackFor = Exception.class)
public synchronized void syncSale(Long productId) {
    TbProduct product = productDao.selectByPrimaryKey(productId);
    if (product == null) {
        throw new RuntimeException("The item was not found.");
    }
    int stock = product.getStock() - 1;
    if (stock >= 0) {
        product.setStock(stock);
        int r = productDao.updateByPrimaryKeySelective(product);
        if (r <= 0) {
            throw new RuntimeException("Merchandise inventory reduction failed."); }}else {
        throw new RuntimeException("Out of stock"); }}Copy the code

It is similar to scheme 1, but synchronized is added in front of the method. Scheme 2 is much better than scheme 1 after testing. However, after several tests, it will be found that the oversold problem still exists, but the probability is lower.

This is because the lock is released but the transaction is not committed, causing multiple threads to read the same value.

So this approach still doesn’t solve the oversold problem.

Scheme 3 (transaction + block lock)

Synchronized can be used to solve the problem of overselling, but performance is affected, and multiple requests need to queue, so use synchronized is not recommended@author cc
 * @dateIn 2021-12-30 he * /
public void manualSale(Long productId) {
    synchronized (this) { sampleSale(productId); }}Copy the code

This is an improved version of plan 2, which solves the problem of plan 2 by putting the lock in a code block.

The downside is that the entire code block is locked, and if there are other time-consuming operations after destocking, other requests can be queued up for a long time.

Scheme 4 (manual SQL)

Oversold problems can also be solved with SQL like this:

update `tb_product` set stock = stock - #{amount} WHERE id = #{productId} AND stock >= #{amount}
Copy the code
InnoDB automatically adds exclusive locks to UPDATE, DELETE, and DELETE statements. InnoDB automatically adds exclusive locks to UPDATE, DELETE, and DELETE statements@author cc
 * @date2021-12-30 15:03 * /
public void sqlSale(Long productId) {
    int amount = 1; // The amount to be deducted
    int r = productDao.updateStockById(productId, amount);
    if (r <= 0) {
        throw new RuntimeException("Merchandise inventory reduction failed."); }}Copy the code

This is because the InnoDB engine automatically adds exclusive locks to UPDATE, DELETE, and DELETE statements, so such statements prevent oversold.

The advantages are obvious, simple and convenient.

The downside is still obvious, as the database has to be manipulated every time, which puts a lot of strain on the system.

Therefore, this scheme is not applicable in high concurrency scenarios.

Scheme 5 (Redis cache)

The disadvantage of plan 4 is hard disk IO, so use Redis to deal with it in memory.

For more information on Redis, see my article: Basic Use of Redis in Spring Boot and elegant interface data caching

With Redis, we cache the item data ahead of time:

redisTemplate.opsForHash().increment("stock"."product_1".1);
Copy the code

There are many ways to cache, not necessarily using hash incr, but this is just an example.

Now that we have an inventory of 1 in Redis, let’s look at a code example:

/** * Normal Redis strategy, put the inventory in the cache, do no other processing * disadvantages: does not support high concurrency, can be oversold *@author cc
 * @dateThe 2021-12-30 14:55 * /
public void redisNormal(Long productId) {
    String productKey = "product_" + productId;
    // Get the inventory of items in the cache
    int stock = Integer.parseInt(redisTemplate.opsForHash().get("stock", productKey).toString());
    // Deduct inventory
    if (stock > 0) {
        redisTemplate.opsForHash().increment("stock", productKey, -1);
    } else {
        throw new RuntimeException("Out of stock");
    }

    // Simulate the time-consuming operation of ordering goods
    try {
        Thread.sleep(2000);
    } catch (Exception e) {
        // Failed to place an order
        System.out.println("Merchandise order failed"); }}Copy the code

We put the item inventory query in memory, which is faster, but the above code is oversold at high concurrency, so we need to lock the query operation.

Scheme 6 (Redis + synchronized)

/** * Redis is synchronized to lock inventory operations@author cc
 * @dateThe 2021-12-30 to * /
public void redisBySync(Long productId) {
    synchronized (this) {
        String productKey = "product_" + productId;
        // Get the inventory of items in the cache
        int stock = Integer.parseInt(redisTemplate.opsForHash().get("stock", productKey).toString());
        // Deduct inventory
        if (stock > 0) {
            redisTemplate.opsForHash().increment("stock", productKey, -1);
        } else {
            throw new RuntimeException("Out of stock"); }}// Simulate the time-consuming operation of ordering goods
    try {
        Thread.sleep(2000);
    } catch (Exception e) {
        System.out.println("Merchandise order failed"); }}Copy the code

Solution 7 (Redis + Lock)

private Lock lock = new ReentrantLock();

/** * Update to the Redis policy to lock inventory operations ** Support high concurrency, more flexible than synchronized **@author cc
 * @dateThe 2021-12-30 neither so did * /
public void redisByLock(Long productId) {
    String result = null;
    lock.lock();
    try {
        String productKey = "product_" + productId;
        // Get the inventory of items in the cache
        int stock = Integer.parseInt(redisTemplate.opsForHash().get("stock", productKey).toString());
        System.out.println("stock: " + stock);

        // Deduct inventory
        if (stock > 0) {
            redisTemplate.opsForHash().increment("stock", productKey, -1);
        } else {
            result = "Out of stock"; }}catch (RuntimeException e) {
        e.printStackTrace();
    } finally {
        lock.unlock();
    }
    if(result ! =null) {
        throw new RuntimeException(result);
    }

    // Simulate the time-consuming operation of ordering goods
    try {
        Thread.sleep(2000);
    } catch (Exception e) {
        System.out.println("Merchandise order failed"); }}Copy the code

The only difference between scheme 6 and Scheme 7 is the way of locking. Compared with synchronized, Lock is more flexible in use, so the use can be decided according to the scene.

Both solutions can solve the oversold problem caused by high concurrency, and the lock is added to the inventory query operation, does not affect the operation of ordering goods, and the use of memory, so the speed is faster.