Implementation of the most complete Java SpringBoot like function in the whole network

Python wechat ordering applet course video

https://edu.csdn.net/course/detail/36074

Python practical quantitative transaction financial management system

https://edu.csdn.net/course/detail/35475

preface

Recently, the company is working on a project of NFT mall, which is roughly a platform that only buys and sells digital products. In the project, there is a demand that users can like products and obtain the total number of likes of products, similar to the following figure

At first, I felt that this function was easy to implement. It was nothing more than adding a praise table. Later, I found that things were not so simple.

The initial design is like this. There are three tables: Commodity table, user table and praise table. When users like, add the user id and commodity id to the praise table and give the number of likes to the corresponding commodities + 1. It seems that there is no problem and the logic is relatively simple, but strange bug s are found during the test. Sometimes the number of likes is incorrect, and the result will be larger than expected.

Key codes are posted below (mybatis plus is used in the project):

public boolean like(Integer userId, Integer productId) {
        // Query whether there are records. If there are records, it will be returned directly
        Like like = getOne(new QueryWrapper().lambda()
 .eq(Like::getUserId, userId)
 .eq(Like::getProductId, productId));
 if(like != null) {
 return true;
 }

 // Save and add 1 to the number of product likes
 save(Like.builder()
 .userId(userId)
 .productId(productId)
 .build());
 return productService.update(new UpdateWrapper().lambda()
 .setSql("like\_count = like\_count + 1")
 .eq(Product::getId, productId));
}

It looks ok, but the data after the test is not correct. Why?

In fact, this is A concurrency problem. As long as it is concurrent, there will be problems. We know that Spring Mvc is based on servlet. After receiving user requests, the servlet will allocate A thread from the thread pool to it, and each request is A separate thread. Imagine if thread A finds that there is no record after executing the query operation, and then cedes control due to CPU scheduling. Then thread B executes the query and finds that there is no record. At this time, threads A and B will perform the operation of saving and adding 1 to the product likes, resulting in incorrect data.

CPU operation sequence: A thread query - > b thread query - > A thread save - > b thread save

Let's use JMeter to simulate the concurrency. Simulate that the user executes 100 likes requests for goods within 1 second. The result should be 1, but the result is 28 (the actual result is not necessarily 28, but it may be any number).

Solution

Bronze plate

Use the synchronized keyword to lock the read-write operation, and release the lock after the operation is completed

public boolean like(Integer userId, Integer productId) {
        String lock = buildLock(userId, productId);
        synchronized (lock) {
            // Query whether there are records. If there are records, it will be returned directly
            Like like = getOne(new QueryWrapper().lambda()
 .eq(Like::getUserId, userId)
 .eq(Like::getProductId, productId), false);
 if(like != null) {
 return true;
 }

 // Save and add 1 to the number of product likes
 save(Like.builder()
 .userId(userId)
 .productId(productId)
 .build());
 return productService.update(new UpdateWrapper().lambda()
 .setSql("like\_count = like\_count + 1")
 .eq(Product::getId, productId));
 }
}

private String buildLock(Integer userId, Integer productId) {
 StringBuilder sb = new StringBuilder();
 sb.append(userId);
 sb.append("::");
 sb.append(productId);
 String lock = sb.toString().intern();

 return lock;
}

It should be noted here that when using a String as a lock, you must call the intern() method. intern() will first find out whether there is the same String from the constant pool. If there is, it will return directly. If not, it will add the current String to the constant pool and then return. If this method is not called, the lock will fail.

JMeter performance data

advantage:

  • The correctness is guaranteed

Disadvantages:

  • The performance is too poor. It can cope with low concurrency. When concurrency is high, the user experience is very poor

Silver plate

Like table user_id and product_id plus union index, and use try catch to catch exceptions to prevent error reporting. Since the joint index is used, there is no need to query before adding. mysql will help us do this.

public boolean like(Integer userId, Integer productId) {
        try {
            // Save and add 1 to the number of product likes
            save(Like.builder()
                    .userId(userId)
                    .productId(productId)
                    .build());
            return productService.update(new UpdateWrapper().lambda()
 .setSql("like\_count = like\_count + 1")
 .eq(Product::getId, productId));
 }catch (DuplicateKeyException exception) {

 }

 return true;
}

JMeter performance data

advantage:

  • The performance is better than the previous scheme

Disadvantages:

  • Regular, no major shortcomings

Gold Edition

Redis is used to cache the like data (the like operation is implemented by lua script to ensure the atomicity of the operation), and then it is synchronized to mysql regularly.

Note: Redis needs to enable persistence. It is better to enable both aof and rdb, otherwise the restart data will be lost

public boolean like(Integer userId, Integer productId) {
        List<String> keys = new ArrayList<>();
        keys.add(buildUserRedisKey(userId));
        keys.add(buildProductRedisKey(productId));

        int value1 = 1;

        redisUtil.execute("lua-script/like.lua", keys, value1);

        return true;
}

private String buildUserRedisKey(Integer userId) {
        return "userId\_" + userId;
}

private String buildProductRedisKey(Integer productId) {
        return "productId\_" + productId;
}

lua script

local userId = KEYS[1]
local productId = KEYS[2]
local flag = ARGV[1] -- 1: Like 0: cancel like


if flag == '1' then
  -- user set Add a product and add 1 to the number of product likes
  if redis.call('SISMEMBER', userId, productId) == 0 then
    redis.call('SADD', userId, productId)
    redis.call('INCR', productId)
  end
else
  -- user set Delete the item and decrease the number of likes of the item by 1
  redis.call('SREM', userId, productId)
  local oldValue = tonumber(redis.call('GET', productId))
  if oldValue and oldValue > 0 then
    redis.call('DECR', productId)
  end
end

return 1

JMeter performance data

advantage:

  • Very good performance

Disadvantages:

  • Too much data and high memory consumption

summary

If there are no performance requirements, the silver version can be used. If there are requirements, the gold version can be used. The problem of large memory consumption can also be solved by some means. For example, some infrequent cache data can be deleted regularly according to business requirements, but correspondingly, when querying, you need to query the database when the query fails.

Source code

Source address: https://github.com/huajiayi/like-demo
Some functions in the source code are not implemented, such as timing synchronization, which needs to be implemented according to business needs

  • Link to this article: https://blog.csdn.net/joeyhua/p/15827149.html
  • About bloggers: comments and private messages will be replied at the first time. perhaps Direct private letter I.
  • Copyright notice: all articles on this blog are in English unless otherwise stated BY-NC-SA License agreement. Reprint please indicate the source!
  • Support bloggers: if you think the article is helpful to you, you can click * * [recommend] (javascript:void(0) in the lower right corner of the article 😉]** once.

Keywords: Python Java Spring Boot computer

Added by env-justin on Sun, 23 Jan 2022 08:45:57 +0200