注意:文章若有错误的地方,欢迎评论区里面指正 🍭 

介绍

  什么是超卖?

        

        在商品库存管理中,超卖是指销售数量超过了实际库存数量的情况。这在电商和其他零售业务中是一个常见的问题。为了防止和解决超卖问题,可以采取以下策略:

1、数据库级别的锁定:
使用数据库的乐观锁或悲观锁来确保在读取和更新库存量时的数据一致性。这可以确保在并发操作中,只有一个操作可以成功修改库存。
2、减少数据库的读写延迟:
使用如Redis这样的内存数据库来缓存库存数据,从而加速读写操作。但需要注意的是,缓存和数据库之间的数据同步问题。
3、分布式锁:
如果你的应用是分布式的,考虑使用分布式锁来确保跨多个实例的库存操作的原子性。
4、队列和限制并发:
使用消息队列来管理库存操作,限制并发的库存更新请求。这可以确保请求按顺序被处理,从而防止超卖。
5、预先分配库存:
在大型促销活动中,可以为每个渠道或每个时间段预先分配一定量的库存。这确保了在活动开始时的并发高峰不会导致超卖。
6、后端验证:
在订单生成之前,再次验证库存数量。即使前端已经进行了检查,后端也应该再次验证以确保数据的准确性。
7、设置库存阈值:
当库存量达到一个预设的阈值时,自动将商品下架或标记为不可售,从而防止进一步的销售。

        总之,防止和解决超卖问题需要结合多种策略和技术手段。重要的是要根据自己的业务场景和技术栈选择最合适的方案,并不断地监控和优化系统以确保数据的准确性和客户的满意度。


一、使用分布式锁

这里我借助Redisson实现分布式锁,Redisson内部封装了分布式锁,可以帮助我们很轻松的实现。

引入依赖

    //springboot版本3.0.4
    <dependency>
      <groupId>org.redisson</groupId>
      <artifactId>redisson-spring-boot-starter</artifactId>
      <version>3.22.0</version>
    </dependency>

     <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-pool2</artifactId>
    </dependency>

application.yml文件

server:
  port: 9888
spring:
  data:
    redis:
      host: 你的redis地址
      port: 6379
      password: 密码
      lettuce:
        pool:
          max-active: 8
          max-wait: -1
          max-idle: 8
          min-idle: 0
        redisson:
        file: classpath:redisson.yml
  datasource:
    url: jdbc:mysql://127.0.0.1/study?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
    driverClassName: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    username: root
    password: 123456
    maxActive: 1000
    initialSize: 100
    maxWait: 60000
    minIdle: 500

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl


redisson.yml


# Redisson 单实例配置
singleServerConfig:
  # 节点地址。格式:redis://host:port
  address: "redis://47.116.126.185:6379"
  # 密码。默认值: null
  password: 123456
  # 数据库编号。默认值: 0
  database: 0
  # 客户端名称(在Redis节点里显示的客户端名称)。默认值: null
  clientName: null
  # 连接超时,单位:毫秒。默认值: 10000
  connectTimeout: 10000
  # 命令等待超时,单位:毫秒。默认值: 3000
  timeout: 3000
  # 命令失败重试次数。默认值: 3
  retryAttempts: 3
  # 命令重试发送时间间隔,单位:毫秒。默认值: 1500
  retryInterval: 1500
  # 最小空闲连接数。默认值: 32
  connectionMinimumIdleSize: 24
  # 连接池大小。默认值: 64
  connectionPoolSize: 64
  # 单个连接最大订阅数量。默认值: 5
  subscriptionsPerConnection: 5
  # 发布和订阅连接的最小空闲连接数。默认值: 1
  subscriptionConnectionMinimumIdleSize: 1
  # 发布和订阅连接池大小。默认值: 50
  subscriptionConnectionPoolSize: 50
  # DNS监测时间间隔,单位:毫秒。默认值: 5000
  dnsMonitoringInterval: 5000
  # 连接空闲超时,单位:毫秒。默认值: 10000
  idleConnectionTimeout: 10000

核心代码:

   

@GetMapping("/{id}")
    public String decStock(@PathVariable("id") Integer id){
        RLock lock = redissonClient.getLock("lock:decStock");
        try {
            lock.lock();
            //查询商品信息
            Products product = productsService.getById(id);
            //获取商品库存
            Integer stockQuantity = product.getStockQuantity();
            if (stockQuantity > 0){
                UpdateWrapper<Products> updateWrapper = new UpdateWrapper<>();
                updateWrapper.eq("id",id).setSql("stock_quantity = stock_quantity - 1");
                boolean result = productsService.update(updateWrapper);
                return "商品库存呢扣减成功!";
            }
            return "商品卖完了!";
        }finally {
        if (lock.isHeldByCurrentThread()) lock.unlock();
        }
    }

二、使用乐观锁

什么是乐观锁?

乐观锁,顾名思义就是总是假设最好的情况,每次获取数据的时候都认为别人不会修改,所以不会上 锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和 CAS 算法实现。        

这里我使用的是CAS来实现的,这种方法利用了数据库的事务性和行锁来确保库存操作的原子性和一致性,从而有效地防止了超卖问题的发生。小伙伴若想使用版本号机制也阔以,版本号法大概思路:在表上添加一个version字段,每次扣减库存先检查version是否修改就可以了。

//CAS方法
 update products set stock_quantity = stock_quantity - 1 where id = #{id} and stock_quantity > 0
@GetMapping("/stock/{id}")
    public String decStocks(@PathVariable("id") Integer id){
        //查询商品信息
        Products product = productsService.getById(id);
        //获取商品库存
        Integer stockQuantity = product.getStockQuantity();
        if (stockQuantity > 0){
            boolean flag = productsService.decStock(id);
            if (flag) return "商品库存呢扣减成功";
        }
        return "商品卖完了!";
    }

这种方式单单使用了乐观锁,虽然解决超卖问题,但是会对数据库造成很大的压力,甚至数据库崩溃,我们可以借助redis缓存去解决这个问题,这个就不跟大家具体演示了,有兴趣的小伙伴可以去实现一下,大致思路:

  1. 将商品库存存入redis中。
  2. 每次扣减之前从redis里面拿出检查
  3. 库存充足,使用increment(key,-1)方法去扣减库存。
  4. 库存不足,直接返回。
  5. 将redis库存同步到数据库

这个方式借助redis是单线程处理,如:若A用户线程先执行redis语句,那么现在库存等于0,后面线程等待,轮到B的时候,B就只能失败,就不会出更新数据库了。

注意:在实现上面思路的时候,可能会遇到redis里面的库存为负数的情况

如两个人同时买这个商品,导致A人第一步时看到还有10个库存,但是B人买9个先处理完逻辑,导致B人的线程10-9=1, A人的线程1-10=-9,则现在需要增加刚刚减去的库存,让别人可以买1个

虽然redis已经防止了超卖,但是数据库层面,为了也要防止超卖,以防redis崩溃时无法使用或者不需要redis处理时,则用乐观锁,因为不一定全部商品都用redis。

超卖问题就先介绍到这里,其实网上有很多成熟的方案,个人推荐大家使用乐观锁+redis原子性操作的方式去解决超卖问题。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐