解决重复下单问题,常提到“一锁二判三更新“到底是什么?

我们在聊到后端并发问题,或者说重复下单问题的时候,经常提到"一锁二判三更新".这个"一锁而判三更新",到底是什么呢?本文田螺哥跟大家聊聊哈!

前言

大家好,我是田螺

我们在聊到后端并发问题,或者说重复下单问题的时候,经常提到"一锁二判三更新".这个"一锁而判三更新",到底是什么呢?本文田螺哥跟大家聊聊哈~~

  • 什么是一锁二判三更新
  • 为什么需要一锁二判三更新?
  • 不同锁策略下的实现差异
1.什么是一锁二判三更新

其实,它是一套处理并发更新数据的标准流程:

  • 一锁:表示先获取锁,保证同一时间只有一个操作能执行
  • 二判:检查数据状态是否符合预期,防止脏更新
  • 三更新:确认无误后执行数据更新操作

比如扣库存的场景,我们来看一个一锁二判三更新的代码例子:

//一锁二判三更新的代码使用例子
@Transactional
public boolean deductStock(Long productId, int quantity) {
    // 一锁:获取商品的行锁
    // 使用for update进行悲观锁锁定,确保同一时间只有一个事务能操作该商品
    Product product = productMapper.selectForUpdateById(productId);
    if (product == null) {
        throw new RuntimeException("商品不存在");
    }
    
    // 二判:判断库存是否充足
    if (product.getStock() < quantity) {
        throw new RuntimeException("库存不足,当前库存:" + product.getStock());
    }
    
    // 三更新:执行库存扣减
    int newStock = product.getStock() - quantity;
    product.setStock(newStock);
    int rows = productMapper.updateById(product);
    
    return rows > 0;
}

对应的 MyBatis update更新方法:

<select id="selectForUpdateById" resultType="com.example.Product">
    select * from product where id = #{id}
    for update
</select>
    2. 为什么需要一锁二判三更新

    在并发场景,为什么需要一锁二判三更新这套使用流程呢?

    假设类似这种场景

    两个用户同时给同一个商品下单,而商品仅剩最后一件库存。如果没有加锁,可能会出现两个订单都创建成功,但实际库存不足的情况。

    同理,也是这个场景,假设你加锁了,如果没有这个二判(判断库存是否充足),依然可能会出现两个订单创建都成功的情况。

    错误使用例子:

    // 错误示例:无锁无判断
    public boolean deductStock(Long productId, int quantity) {
        // 1. 查询当前库存
        Product product = productMapper.selectById(productId);
        
        // 2. 直接扣减库存(未判断是否充足)
        int newStock = product.getStock() - quantity;
        product.setStock(newStock);
        
        // 3. 更新库存
        return productMapper.updateById(product) > 0;
    }

    因此,"一锁二判三更新" 正是为这类并发场景设计的解决方案。单独使用锁或单独做判断都无法彻底解决问题,必须三者结合。

    3. 不同锁策略下的实现差异

    一锁二判三更新中的一锁,其实有不同的实现方式的,既有悲观锁,也有乐观锁。

    对于悲观锁,适合写操作比较频繁、冲突概率高的场景:

    • 优点:实现简单,冲突处理直接
    • 缺点:可能导致锁等待,并发性能较低

    比如,我们前面第一小节的就是悲观锁哈实现方式哈

    <select id="selectForUpdateById" resultType="com.example.Product">
        select * from product where id = #{id}
        for update
    </select>

      如果读操作频繁、写操作冲突概率低的场景,则更适合用乐观锁。简单demo代码如下:

      // 乐观锁实现:一锁(版本控制)二判三更新
      public boolean deductStock(Long productId, int quantity) {
          // 获取当前商品信息(包含版本号)
          Product product = productMapper.selectById(productId);
          if (product == null) {
              throw new RuntimeException("商品不存在");
          }
          
          // 二判:判断库存是否充足
          if (product.getStock() < quantity) {
              throw new RuntimeException("库存不足");
          }
          
          // 准备更新数据
          int newStock = product.getStock() - quantity;
          product.setStock(newStock);
          // 版本号+1(乐观锁的关键)
          product.setVersion(product.getVersion() + 1);
          
          // 执行更新(WHERE条件包含版本号,相当于一锁的实现)
          int rows = productMapper.updateWithVersion(product);
          
          // 如果更新行数为0,说明版本号已变,并发冲突
          if (rows == 0) {
              throw new RuntimeException("并发更新冲突,请重试");
          }
          
          returntrue;
      }

        很多时候,解决并发问题,我们使用的是Redis分布式锁。

        在分布式系统中,"一锁" 通常会升级为分布式锁,而 "二判" 在库存场景下核心就是判断库存是否满足需求。

        以 Redis 分布式锁为例,实现分布式环境下的库存扣减简单代码如下:

        // 分布式环境下的"一锁二判三更新"
        public boolean deductStock(Long productId, int quantity) {
            // 分布式锁的key,通常用业务标识+ID
            String lockKey = "stock:lock:" + productId;
            // 生成唯一标识,用于释放锁时的身份验证
            String requestId = UUID.randomUUID().toString();
            
            try {
                // 一锁:获取分布式锁(演示用的)
                // 第三个参数是超时时间,防止死锁
                boolean locked = redisTemplate.opsForValue()
                    .setIfAbsent(lockKey, requestId, 10, TimeUnit.SECONDS);
                
                if (!locked) {
                    // 获取锁失败,说明有其他进程正在操作,返回失败或重试
                    returnfalse;
                }
                
                // 二判:查询并判断库存
                Product product = productMapper.selectById(productId);
                if (product == null) {
                    throw new RuntimeException("商品不存在");
                }
                if (product.getStock() < quantity) {
                    throw new RuntimeException("库存不足,当前库存:" + product.getStock());
                }
                
                // 三更新:扣减库存
                int newStock = product.getStock() - quantity;
                product.setStock(newStock);
                int rows = productMapper.updateById(product);
                
                return rows > 0;
            } finally {
                // 释放分布式锁(需要验证身份,防止误删其他进程的锁)
                if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) {
                    redisTemplate.delete(lockKey);
                }
            }
        }

        AI大模型学习福利

        作为一名热心肠的互联网老兵,我决定把宝贵的AI知识分享给大家。 至于能学习到多少就看你的学习毅力和能力了 。我已将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。

        一、全套AGI大模型学习路线

        AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!

        因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获取

        二、640套AI大模型报告合集

        这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。

        因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获

        三、AI大模型经典PDF籍

        随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。


        因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获

        四、AI大模型商业化落地方案

        因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获

        作为普通人,入局大模型时代需要持续学习和实践,不断提高自己的技能和认知水平,同时也需要有责任感和伦理意识,为人工智能的健康发展贡献力量

        评论
        成就一亿技术人!
        拼手气红包6.0元
        还能输入1000个字符
         
        红包 添加红包
        表情包 插入表情
         条评论被折叠 查看
        添加红包

        请填写红包祝福语或标题

        红包个数最小为10个

        红包金额最低5元

        当前余额3.43前往充值 >
        需支付:10.00
        成就一亿技术人!
        领取后你会自动成为博主和红包主的粉丝 规则
        hope_wisdom
        发出的红包
        实付
        使用余额支付
        点击重新获取
        扫码支付
        钱包余额 0

        抵扣说明:

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

        余额充值