preface

From the beginning of this article, Laomao will share the evolution process of lock in practical application scenarios through the business scenarios of e-commerce. From Java singleton locking to the practice of locking in distributed environment.

The first case of oversold phenomenon

In fact, in the e-commerce business scenario, there will be such a taboo phenomenon, that is, “oversold”, then what is oversold? For example, if there are only 10 items in stock, 15 items are sold. In short, the number of items sold exceeds the number of items in stock. “Oversold” will cause merchants to have no goods to deliver, the delivery time is extended, from causing disputes between the two sides of the transaction.

We come together to analyze the causes of this phenomenon: if the goods is only the last one, the user A and user B, see the goods at the same time, and joined the shopping cart to submit their orders at the same time, the two users read at the same time the number of goods in inventory for A respective memory after deduction, to update the database. Therefore, oversold occurs. Let’s take a look at the process diagram in detail:

The solution

How do we solve these problems on a single server? Let’s look at the specific scheme. As mentioned in the previous description, when we subtract inventory, we do it in memory. We then sink it into the database to update the inventory. We can pass the inventory increment to the database, subtract one inventory increment by -1, and solve the concurrency problem with the UPDATE row lock while the database calculates the inventory with the update statement. (Database row lock: when the database is updated, the current row is locked, that is, the row lock, here the old cat description is simple, interested friends can study the database lock). Let’s look at a concrete code example.

The business logic code is as follows:

@Service
@Slf4j
public class OrderService {
    @Resource
    private KdOrderMapper orderMapper;
    @Resource
    private KdOrderItemMapper orderItemMapper;
    @Resource
    private KdProductMapper productMapper;
    // Purchase item ID
    private int purchaseProductId = 100100;
    // Number of items purchased
    private int purchaseProductNum = 1;

    @Transactional(rollbackFor = Exception.class)
    public Integer createOrder(a) throws Exception{
        KdProduct product = productMapper.selectByPrimaryKey(purchaseProductId);
        if (product==null) {throw new Exception("Purchase goods:"+purchaseProductId+"Doesn't exist.");
        }

        // Current inventory of goods
        Integer currentCount = product.getCount();
        // Check inventory
        if (purchaseProductNum > currentCount){
            throw new Exception("Goods"+purchaseProductId+"Only"+currentCount+"Piece, cannot be purchased.");
        }
        // Calculate the remaining inventory
        Integer leftCount = currentCount -purchaseProductNum;
        product.setCount(leftCount);
        product.setTimeModified(new Date());
        product.setUpdateUser("kdaddy");
        productMapper.updateByPrimaryKeySelective(product);
        // Generate the order
        KdOrder order = new KdOrder();
        order.setOrderAmount(product.getPrice().multiply(new BigDecimal(purchaseProductNum)));
        order.setOrderStatus(1);/ / to be processed
        order.setReceiverName("kdaddy");
        order.setReceiverMobile("13311112222");
        order.setTimeCreated(new Date());
        order.setTimeModified(new Date());
        order.setCreateUser("kdaddy");
        order.setUpdateUser("kdaddy");
        orderMapper.insertSelective(order);

        KdOrderItem orderItem = new KdOrderItem();
        orderItem.setOrderId(order.getId());
        orderItem.setProductId(product.getId());
        orderItem.setPurchasePrice(product.getPrice());
        orderItem.setPurchaseNum(purchaseProductNum);
        orderItem.setCreateUser("kdaddy");
        orderItem.setTimeCreated(new Date());
        orderItem.setTimeModified(new Date());
        orderItem.setUpdateUser("kdaddy");
        orderItemMapper.insertSelective(orderItem);
        returnorder.getId(); }}Copy the code

What we can see from the above code is that the inventory deduction is done in memory. So let’s look at the specific unit test code:

@SpringBootTest
class DistributeApplicationTests {
    @Autowired
    private OrderService orderService;

    @Test
    public void concurrentOrder(a) throws InterruptedException {
        // A counter
        CountDownLatch cdl = new CountDownLatch(5);
        // It is used to wait for five threads to be concurrent
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5);

        ExecutorService es = Executors.newFixedThreadPool(5);
        for (int i =0; i<5; i++){ es.execute(()->{try {
                    // Waiting for five concurrent threads
                    cyclicBarrier.await();
                    Integer orderId = orderService.createOrder();
                    System.out.println("Order ID:"+orderId);
                } catch (Exception e) {
                    e.printStackTrace();
                }finally{ cdl.countDown(); }}); }// Avoid shutting down the database connection pool earlycdl.await(); es.shutdown(); }}Copy the code

After executing the code, let’s look at the result:

Order ID: 1 Order ID: 2 Order ID: 3 Order ID: 4 Order ID: 5Copy the code

Obviously, there is only one inventory in the database, but five order records are generated, as shown below:

This also produces the phenomenon of oversold, so how can solve this problem?

In the single architecture, the database row lock is used to solve the problem of e-commerce oversold.

So if this is the solution, we will have to sink our inventory deduction action into our database, using the database row lock to solve the problem of simultaneous operation, let’s look at the code change point.

@Service
@Slf4j
public class OrderServiceOptimizeOne {... Space is limited, omitted here, refer to github source code@Transactional(rollbackFor = Exception.class)
    public Integer createOrder(a) throws Exception{
        KdProduct product = productMapper.selectByPrimaryKey(purchaseProductId);
        if (product==null) {throw new Exception("Purchase goods:"+purchaseProductId+"Doesn't exist.");
        }

        // Current inventory of goods
        Integer currentCount = product.getCount();
        // Check inventory
        if (purchaseProductNum > currentCount){
            throw new Exception("Goods"+purchaseProductId+"Only"+currentCount+"Piece, cannot be purchased.");
        }

        // Complete the decrement in the database
        productMapper.updateProductCount(purchaseProductNum,"kd".new Date(),product.getId());
        // Generate the order. Space is limited, omitted here, refer to github source codereturnorder.getId(); }}Copy the code

Let’s look at the results of the implementation

From the above results, we find that our order quantity is still 5 orders, but the inventory quantity is no longer 0, but changed from 1 to -4. Obviously, such a result is still not what we want, so this is another phenomenon of oversold. Let’s look at the causes of oversold phenomenon number two.

Case of the second phenomenon of oversold

The above is actually the second phenomenon, so what is the cause? In fact, there was a problem when checking the inventory. During checking the inventory, the inventory was checked concurrently. Five threads got the inventory at the same time and found that the inventory quantity was 1, which caused the illusion of sufficient inventory. In this case, because the write operation has the update lock, the deduction operation is performed in sequence, and the deduction operation does not have verification logic. Hence the oversold appearance. The simple picture is as follows:

Solution 1:

In the single architecture, the database row lock is used to solve the problem of e-commerce oversold. For the current case, in fact, our solution is relatively simple, that is, after the update, we immediately check whether the quantity of inventory is greater than or equal to 0. If it’s negative, we just throw an exception. (Of course, because this operation does not involve the knowledge of lock, so this scheme is only proposed, not the actual code practice)

Solution 2:

Check inventory and subtract inventory unified lock, so that it becomes atomic operation, concurrent only when the lock will read the inventory and subtract inventory operation. When the deduction is over, release the lock to make sure the inventory doesn’t go negative. Synchronized and ReentrantLock are the two Java lock keywords mentioned in the previous blog post.

The usage of synchronized keyword has been mentioned in the previous blog. There are two methods of synchronized keyword: method lock and code block lock. Let’s take a look at the code through practice.

// 'synchronized' method block lock
@Service
@Slf4j
public class OrderServiceSync01 {... Space is limited, omitted here, refer to github source code@Transactional(rollbackFor = Exception.class)
    public synchronized Integer createOrder(a) throws Exception{
        KdProduct product = productMapper.selectByPrimaryKey(purchaseProductId);
        if (product==null) {throw new Exception("Purchase goods:"+purchaseProductId+"Doesn't exist.");
        }

        // Current inventory of goods
        Integer currentCount = product.getCount();
        // Check inventory
        if (purchaseProductNum > currentCount){
            throw new Exception("Goods"+purchaseProductId+"Only"+currentCount+"Piece, cannot be purchased.");
        }

        // Complete the decrement in the database
        productMapper.updateProductCount(purchaseProductNum,"kd".new Date(),product.getId());
        // Generate the order. Space is limited, omitted here, refer to github source codereturnorder.getId(); }}Copy the code

Now let’s look at the result of the run.

[pool-1-thread-2] c.k.d.service.OrderServiceSync01 : - the thread pool - 1-2 inventory number 1 [] - thread pool - 1-1 C.K.D.S ervice. OrderServiceSync01: - the thread pool - 1-1 inventory order id number 1:12 [- thread pool - 1-5] C.K.D.S ervice. OrderServiceSync01: - the thread pool - 1-5 inventory order id number - 1:13 [- thread pool - 1-3] C.K.D.S ervice. OrderServiceSync01: - thread pool - 1-3 inventory number 1Copy the code

At this point it became clear that there was still a problem with the data, so what was the cause?

In fact, the smart guys have already figured out that our second thread is still reading 1, so why? The reason the second thread reads the inventory is 1 is because the transaction from the previous thread did not commit, and we can clearly see that the current transaction on our method is outside the lock. So that’s where the problem comes in. So for this problem, we can actually commit the transaction manually and put it in the lock block. The specific transformation is as follows.

 public synchronized Integer createOrder(a) throws Exception{
     // Manually get the current transaction
     TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);
        KdProduct product = productMapper.selectByPrimaryKey(purchaseProductId);
        if (product==null){
            platformTransactionManager.rollback(transaction);
            throw new Exception("Purchase goods:"+purchaseProductId+"Doesn't exist.");
        }

        // Current inventory of goods
        Integer currentCount = product.getCount();
        log.info(Thread.currentThread().getName()+"Inventory number"+currentCount);
        // Check inventory
        if (purchaseProductNum > currentCount){
            platformTransactionManager.rollback(transaction);
            throw new Exception("Goods"+purchaseProductId+"Only"+currentCount+"Piece, cannot be purchased.");
        }

        // Complete the decrement in the database
        productMapper.updateProductCount(purchaseProductNum,"kd".new Date(),product.getId());
        // Generate the order and finish saving the order. Space limit, is omitted, specific lot for reference source platformTransactionManager.com MIT (transaction);return order.getId();
    }
Copy the code

Now let’s look at the result of the run:

[pool-1-thread-3] c.k.d.service.OrderServiceSync01 : - the thread pool - 1-3 inventory number 1 [- thread pool - 1-5] C.K.D.S ervice. OrderServiceSync01: - the thread pool - 1-5 inventory number 0 order id: 16 [- thread pool - 1-4] C.K.D.S ervice. OrderServiceSync01: - the thread pool - 1-4 inventory Numbers 0 [] - thread pool - 1-1 C.K.D.S ervice. OrderServiceSync01: inventory Numbers 0 - thread pool - 1-1Copy the code

From the above result, we can clearly see that only the first thread fetched inventory 1, and all subsequent threads fetched inventory 0. Let’s look at the actual database.

It is clear that we have the correct inventory and order quantity to this database.

Behind synchronized code block lock and ReentrantLock to partners to try to complete, of course, the old cat has also written the relevant code. The specific source address is: github.com/maoba/kd-di…

Write in the last

This article through the e-commerce in two kinds of oversold phenomenon and partners to share a single lock to solve the problem process. Of course, this type of lock is not used across JVMS and will not work with multiple JVMS, so the implementation of distributed locks will be shared in a later article. Of course, it is also through the example of overselling in e-commerce to share with you.

In addition, I hope you don’t waste your time with me. If you can help me, I hope to get your praise and share.