Frequently asked questions about the seckill system—how to avoid oversold inventory?

Hello everyone! I am Sum Mo, a first-line low-level code farmer. I usually like to study and think about some technical-related issues and organize them into articles. I am limited to my level. If there are any inappropriate expressions in the articles and codes, please feel free to enlighten me.

The following is the text!

first look at the problem

First a string of code

public String buy(Long goodsId, Integer goodsNum) {
    
    
    //查询商品库存
    Goods goods = goodsMapper.selectById(goodsId);
    //如果当前库存为0,提示商品已经卖光了
    if (goods.getGoodsInventory() <= 0) {
    
    
        return "商品已经卖光了!";
    }
    //如果当前购买数量大于库存,提示库存不足
    if (goodsNum > goods.getGoodsInventory()) {
    
    
        return "库存不足!";
    }
    //更新库存
    goods.setGoodsInventory(goods.getGoodsInventory() - goodsNum);
    goodsMapper.updateById(goods);
    return "购买成功!";
}

Let's take a look at this string of code. The logic is expressed in a flow chart as follows:


From the picture, the logic is still very clear, and if the single test is performed, no bugs can be found. But in the flash kill scenario, the problem can become big. 100 items may be sold for 1,000 orders, resulting in serious capital losses . Now it is really necessary to kill a programmer to sacrifice to heaven.

problem analysis

Under normal circumstances, if the requests come one after another, there will be no problem with this string of codes, as shown in the figure below:

For different requests at different times, the inventory of goods obtained each time is updated, and the logic is ok.

So why is there an oversold problem?
First of all, we add a scene to this string of codes: product flash sale (it is difficult to reproduce the oversold problem in non-spike scene).
The characteristics of the seckill scene are as follows:

  • High concurrency processing: In the flash sale scenario, a large number of shoppers may flood into the system at the same time, so it is necessary to have high concurrency processing capabilities to ensure that the system can withstand high concurrent access and provide fast response.
  • Quick response: In the flash sale scenario, due to time constraints and fierce competition, the system needs to be able to quickly respond to shoppers' requests, otherwise the purchase may fail and affect the shopper's shopping experience.
  • Distributed system: In the flash sale scenario, a single server cannot handle the peak request. The distributed system can improve the system's fault tolerance and pressure resistance, which is very suitable for the flash sale scenario.

In this scenario, the requests cannot be one after the other, but tens of thousands of requests come at the same time, then there will be multiple requests to query the inventory at the same time, as shown in the following figure:

If the commodity inventory table is queried at the same time, the obtained commodity inventory must be the same, and the judgment logic is also the same.

For example, the current inventory of the product is 10 pieces. Request 1 buys 6 pieces, and Request 2 buys 5 pieces. Since the inventory of the two requests is 10, it must be sold.
But the real situation is 5+6=11>10, obviously there is a problem! Of these two requests, one of them must fail!

那么,这种问题怎么解决呢?

solution

Judging from the above example, the problem seems to be that the inventory is oversold every time we get it . Doesn’t this problem solve the problem 库存都是一样的as long as we get it every time ?库存都是最新

Before talking about the plan, first post my test table structure:

CREATE TABLE `t_goods` (
  `id` bigint NOT NULL COMMENT '物理主键',
  `goods_name` varchar(64) DEFAULT NULL COMMENT '商品名称',
  `goods_pic` varchar(255) DEFAULT NULL COMMENT '商品图片',
  `goods_desc` varchar(255) DEFAULT NULL COMMENT '商品描述信息',
  `goods_inventory` int DEFAULT NULL COMMENT '商品库存',
  `goods_price` decimal(10,2) DEFAULT NULL COMMENT '商品价格',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Method 1, redis distributed lock

Introduction to Redisson

Official introduction: Redisson is a Redis-based Java resident memory data grid (In-Memory Data Grid). It encapsulates the Redis client API and provides commonly used data structures and services such as distributed locks, distributed collections, distributed objects, and distributed Maps. Redisson supports Java 6 and above and Redis 2.6 and above, and uses codecs and serializers to support any object type. Redisson also provides some advanced features, such as asynchronous API and reactive streaming API. It can be used in distributed systems to achieve data processing with high availability, high performance, and high scalability.

Redisson uses

introduce

<!--使用redisson作为分布式锁-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.16.8</version>
</dependency>

inject object

RedissonConfig.java

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {
    
    
    /**
     * 所有对Redisson的使用都是通过RedissonClient对象
     *
     * @return
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient() {
    
    
        // 创建配置 指定redis地址及节点信息
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("123456");

        // 根据config创建出RedissonClient实例
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;

    }
}

Code optimization

public String buyRedisLock(Long goodsId, Integer goodsNum) {
    
    
    RLock lock = redissonClient.getLock("goods_buy");
    try {
    
    
        //加分布式锁
        lock.lock();
        //查询商品库存
        Goods goods = goodsMapper.selectById(goodsId);
        //如果当前库存为0,提示商品已经卖光了
        if (goods.getGoodsInventory() <= 0) {
    
    
                return "商品已经卖光了!";
        }
        //如果当前购买数量大于库存,提示库存不足
        if (goodsNum > goods.getGoodsInventory()) {
    
    
                return "库存不足!";
        }
        //更新库存
        goods.setGoodsInventory(goods.getGoodsInventory() - goodsNum);
        goodsMapper.updateById(goods);
        return "购买成功!";
    } catch (Exception e) {
    
    
        log.error("秒杀失败");
    } finally {
    
    
        lock.unlock();
    }
    return "购买失败";
}

After adding the Redisson distributed lock, the request is changed from asynchronous to synchronous, allowing purchase operations to be carried out one by one, which solves the problem of oversold inventory, but it will increase the waiting time for users and affect the user experience.

Method 2, MySQL row lock

Introduction to row locks

MySQL's row lock is a lock for row-level data. It can lock a row of data in a table to ensure that other transactions cannot modify the row data during the locking period, thereby ensuring data consistency and integrity.
The features are as follows:

  • MySQL's row locks can only be used in the InnoDB storage engine.
  • Row locks require an index to be implemented, otherwise the entire table will be automatically locked.
  • Row locks can be used explicitly by using the "SELECT ... FOR UPDATE" and "SELECT ... LOCK IN SHARE MODE" statements.

In short, row locks can effectively ensure the consistency and integrity of data, but too many row locks can also cause performance problems, so careful consideration is required when using row locks to avoid performance bottlenecks.

So back to the issue of oversold inventory, we can add a row lock when we first check the inventory of the product. The implementation is very simple, that is, the

 //查询商品库存
Goods goods = goodsMapper.selectById(goodsId);

原始查询SQL
SELECT *
  FROM t_goods
  WHERE id = #{
    
    goodsId}

改写为
 SELECT *
  FROM t_goods
  WHERE id = #{
    
    goodsId} for update

Then the queried product inventory information will be locked, and other requests need to wait for the current request to end when they want to read this row of data, so that the inventory is up to date every time they are queried. However, like the Redisson distributed lock, it will make the user wait longer and affect the user experience.

Method 3. Optimistic locking

The optimistic locking mechanism is similar to the cas mechanism in java. It does not lock when querying data. Only when updating data does it compare whether the data has changed. If there is no change, perform an update operation, and if it has changed, retry.

Add a version field to the product table and initialize the data to 0

`version` int(11) DEFAULT NULL COMMENT '版本'

Modify the update SQL as follows

update t_goods
set goods_inventory = goods_inventory - #{goodsNum},
     version         = version + 1
where id = #{goodsId}
and version = #{version}

The Java code is modified as follows

public String buyVersion(Long goodsId, Integer goodsNum) {
    
    
    //查询商品库存(该语句使用了行锁)
    Goods goods = goodsMapper.selectById(goodsId);
    //如果当前库存为0,提示商品已经卖光了
    if (goods.getGoodsInventory() <= 0) {
    
    
        return "商品已经卖光了!";
    }
    if (goodsMapper.updateInventoryAndVersion(goodsId, goodsNum, goods.getVersion()) > 0) {
    
    
      return "购买成功!";
    }
    return "库存不足!";
}

By adding the control of the version number, the version number is compared in the where condition when deducting the inventory. Which record is queried, then which record is required to be updated, and the version number cannot be changed during the process of querying the update, otherwise the update will fail.

Method 4, where conditions and unsigned non-negative field restrictions

The previous Redisson distributed locks and row locks are used to solve the oversold problem. Let’s change the way of thinking: can this problem be solved 通过每次都拿到最新的库存when the inventory is deducted ? The answer is yes. Back to the code above:库存一定大于购买量

 //更新库存
goods.setGoodsInventory(goods.getGoodsInventory() - goodsNum);
goodsMapper.updateById(goods);

We wrote the deduction of inventory in the code, which is definitely not possible, because the inventory we get may be the same in the distributed system, we should put the deduction logic of the inventory into SQL, namely:

 update t_goods
 set goods_inventory = goods_inventory - #{goodsNum}
 where id = #{goodsId}

The above SQL ensures that the inventory obtained every time is the inventory of the database, but we need to add a judgment: to ensure that the inventory is greater than the purchase amount, that is:

update t_goods
set goods_inventory = goods_inventory - #{goodsNum}
where id = #{goodsId}
AND (goods_inventory - #{goodsNum}) >= 0

Then the Java code above also needs to be modified:

public String buySqlUpdate(Long goodsId, Integer goodsNum) {
    
    
    //查询商品库存(该语句使用了行锁)
    Goods goods = goodsMapper.queryById(goodsId);
    //如果当前库存为0,提示商品已经卖光了
    if (goods.getGoodsInventory() <= 0) {
    
    
        return "商品已经卖光了!";
    }
    //此处需要判断更新操作是否成功
    if (goodsMapper.updateInventory(goodsId, goodsNum) > 0) {
    
    
        return "购买成功!";
     }
    return "库存不足!";
}

Another method is the same as the where condition, which is to limit the unsigned non-negative field, and set the inventory field to the unsigned non-negative field type, so that the deduction will not be negative.

in conclusion

solution advantage shortcoming
redis distributed lock Redis distributed lock can solve the lock problem in distributed scenarios, guarantee the access sequence and security of multiple nodes to the same resource, and have high performance. Single point of failure problem, if the Redis node goes down, it will cause the lock to become invalid.
MySQL row lock It can guarantee the isolation of transactions and avoid data conflicts under concurrency. The performance is low, which has a great impact on the performance of the database, and there is also a deadlock problem.
optimistic lock Compared with pessimistic locking, optimistic locking does not block threads and has higher performance. Additional version control fields are required, and concurrency conflicts are prone to occur in high concurrency situations.
where conditions and unsigned non-negative field restrictions The where condition and the unsigned non-negative field limit can be used to ensure that the inventory will not be oversold, which is simple and easy to implement. There may be certain security risks, and if some operations are not properly restricted, it may still cause inventory oversold. At the same time, if some scenarios require multiple update operations on the inventory, restrictions may cause the operation to fail, and the data needs to be queried again, which will affect performance.

There are many solutions. From the perspective of usage combined with actual business, there is no optimal, only better, and several solutions can even be combined to solve the problem.

This is the end of the full text, goodbye!

Guess you like

Origin blog.csdn.net/weixin_33005117/article/details/130845355