【学习总结】使用分布式锁和乐观锁解决“超卖”问题

本文探讨了电商中的超卖问题及其解决方案,包括数据库级别锁定、使用Redis缓存、分布式锁、队列限流和后端验证等策略,以及乐观锁和分布式锁的具体实现方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

介绍

  什么是超卖?

        

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

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原子性操作的方式去解决超卖问题。

<think>好的,我现在需要回答用户关于分布式锁乐观锁悲观锁的概念、区别、原理及使用场景的问题。首先,我得回忆一下这三种锁的基本定义,然后根据提供的引用资料来整理信息,确保准确性全面性。 首先,悲观锁。根据引用[1][2][4],悲观锁的核心思想是假设最坏的情况,每次操作数据时都认为会有其他并发操作,因此先加锁再操作。数据库中的SELECT FOR UPDATE就是典型例子,适用于写操作频繁的场景,比如账户余额更新,避免数据冲突,但可能降低并发性能。 然后是乐观锁,引用[1][3][4]提到,乐观锁假设冲突较少,通过版本号或时间戳机制实现,比如CAS操作。适用于读多写少的情况,比如库存扣减,先操作后检查版本,减少了锁的开销,但需要处理更新失败的情况。 分布式锁,引用[1][3][4]指出,用于分布式系统中跨进程的资源控制,Redis的Redisson实现就是例子,支持可重入、自动续期,适用于秒杀系统或分布式任务调度,相比数据库锁性能更好,但需要处理网络延迟等问题。 接下来是区别部分。原理上,悲观锁依赖数据库锁,乐观锁用版本号,分布式锁用外部存储如Redis。使用场景方面,根据引用[3][4],悲观锁适合高并发写,乐观锁适合读多写少,分布式锁适合跨系统。性能上,乐观锁分布式锁通常比悲观锁高效,但各有适用条件。 需要确保回答结构清晰,分点说明,并引用相关资料。同时要生成相关问题,可能涉及应用场景选择、实现方式对比等。最后检查引用标识是否正确,避免遗漏。可能还需要注意用户提供的引用中的一些细节,比如Redisson的具体功能,以及不同锁的适用场景对比。</think>### 分布式锁乐观锁与悲观锁详解 #### 一、基本概念与原理 1. **悲观锁** - **原理**:假设并发操作必然发生冲突,通过**先加锁后操作**的方式确保数据独占性。例如数据库的 `SELECT ... FOR UPDATE` 语句会直接锁定记录[^2][^4]。 - **实现**:依赖数据库锁机制(如行锁、表锁),或编程语言的互斥锁(如Java的 `synchronized`)。 2. **乐观锁** - **原理**:假设冲突概率低,通过**版本控制**(如时间戳、版本号)实现无锁化操作。例如更新数据时检查版本是否匹配,若冲突则重试或回滚[^3][^4]。 - **实现**:常用CAS(Compare And Swap)算法,如MySQL的 `UPDATE table SET value=new_value, version=version+1 WHERE id=1 AND version=old_version`。 3. **分布式锁** - **原理**:在分布式系统中协调多个节点对共享资源的访问,通常基于**外部存储系统**(如Redis、ZooKeeper)实现锁的互斥性。例如Redisson的 `RLock` 通过Lua脚本保证原子性[^1][^3]。 - **实现**:Redis的 `SETNX` 指令、Redisson的可重入锁、ZooKeeper的临时有序节点。 --- #### 二、核心区别 | 维度 | 悲观锁 | 乐观锁 | 分布式锁 | |--------------|-------------------------|-------------------------|--------------------------| | **冲突假设** | 高概率冲突 | 低概率冲突 | 跨进程/节点冲突 | | **加锁时机** | 操作前加锁 | 操作后检查版本 | 通过外部系统协调加锁 | | **性能影响** | 高(阻塞其他操作) | 低(无锁,可能需重试) | 中等(依赖网络通信) | | **适用场景** | 写操作频繁(如转账)[^2] | 读多写少(如库存扣减)[^3] | 分布式任务调度、秒杀系统[^1] | --- #### 三、典型应用场景 1. **悲观锁** - 银行账户余额更新(避免并发扣款冲突)[^2]。 - 订单状态修改(确保同一订单不会被重复处理)。 2. **乐观锁** - 电商库存扣减(版本号防止)[^3]。 - 文档协同编辑(通过时间戳检测内容冲突)。 3. **分布式锁** - 分布式定时任务(防止多节点重复执行)[^1]。 - 秒杀系统中商品抢购(Redis分布式锁控制并发请求)。 --- #### 四、选型建议 - **单体应用**:优先选择数据库悲观锁(简单可靠)[^3]。 - **高并发读场景**:使用乐观锁减少锁开销[^4]。 - **分布式系统**:Redis分布式锁(性能高,支持自动续期)或ZooKeeper(强一致性)[^1][^3]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小杰不秃头

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值