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
-
__EOF__
- **Author:** [JoeyHua](https://blog.csdn.net/biggbang)
- 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.