高并发场景秒杀抢购超卖BUG

场景: 多个请求同时读取到剩余库存,各自判断库存容量符合抢购条件,导致减库存操作执行异常。

public String deductStock() {
  int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")  
  if (stock > 0) {  
      int realStock = stock - 1;  
      stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)  
      System.out.println("扣减成功,剩余库存:" \+ realStock);  
  } else {  
    System.out.println("扣减失败,库存不足");  
  }
  return "end";
} 

当多个请求同时扣减库存时导致最后库存不准确。

解决方式:jvm级锁与分布式锁

高并发场景下jvm级锁与分布式锁:

synchronized: 在分布式架构中,当请求分配到不同服务器上时,每个服务器上的jvm锁都会认为自己获得了锁,允许请求进入,导致锁失效。
分布式锁:把锁从每个进程内部移到所有进程都能访问的外部独立系统中,让所有服务器竞争同一把锁。

分布式锁简单版本

命令:SETNX key value:如果key不存在,将key的值设置为value,存在则不做任何操作。

public String deductStock() {  
    String lockKey = "lock:product_101";  
    Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "xxx");  
    if (!result)   
        return "error_code";  
    try {  
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")  
        if (stock > 0) {  
            int realStock = stock - 1;  
            stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)  
            System.out.println("扣减成功,剩余库存:" + realStock);  
        } else {  
            System.out.println("扣减失败,库存不足");  
        }  
    } finally {  
            stringRedisTemplate.delete(lockKey);  
    }  
    return "end";  
}
//  try--catch此时作用-- 防止程序执行时出现异常导致锁未释放,导致死锁。此时未设置超时时间。
分布式锁设置超时时间

添加代码 :

stringRedisTemplate.expire(lockKey,10, TimeUnit.SECONDS);

当前问题: 加锁与设置超时时间代码在redis执行性无法保证一起顺序执行。redis就执行命令单线程的,命令需要排队执行,执行这两个命令时可能被其他请求执行命令进行加塞(原子性问题)

原子性:单线程保证

使用组合指令

代码如下:

public String deductStock() {  
    String lockKey = "lock:product_101";  
    Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "xxx",10,TimeUnit.SECONDS);  
    if (!result)  
        return "error_code";  
 
    try {  
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")  
        if (stock > 0) {  
            int realStock = stock - 1;  
            stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)  
            System.out.println("扣减成功,剩余库存:" \+ realStock);  
        } else {  
            System.out.println("扣减失败,库存不足");  
        }  
    } finally {  
            stringRedisTemplate.delete(lockKey);  
    }  
    return "end";  
}

并发量大时,依然存在锁失效问题

  假设3个线程 threadA,threadB,threadC,threadA获取锁后10s未执行完库存扣减逻辑,此时锁已经失效,threadB获取到锁开始执行5s,threadA同时继续执行5s且执行释放锁逻辑,此时ThreadA释放threadB的锁。threadC获取到锁开始执行5s,threadB同时继续执行5s且执行释放锁逻辑,此时ThreadB释放threadC的锁。导致锁一直是失效状态。

在高并发场景线程执行顺序完全不可预料,上述只是极端情况。
问题根本点:当前线程加的锁被其他线程释放 .
解决:给每个线程分配唯一ID。(不可以用ThreadId,因为每一台服务器可能存在相同的ThreadId)
代码如下

public String deductStock() {  
    String lockKey = "lock:product_101";  
    String clientId = UUID.randomUUID().toString();  //唯一id 
    Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId,10,TimeUnit.SECONDS);  
    if (!result)  
        return "error_code";  
    try {  
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")  
        if (stock > 0) {  
            int realStock = stock - 1;  
            stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)  
            System.out.println("扣减成功,剩余库存:" + realStock);  
        } else {  
            System.out.println("扣减失败,库存不足");  
        }  
    } finally {  
        if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {   // 通过唯一id区分是否是当前线程的锁
            stringRedisTemplate.delete(lockKey);  //删除锁
        }  
    }  
    return "end";  
}
此时依然存在锁失效问题:
    当ThreadA在执行区分是否是当前线程的锁逻辑后出现卡顿,在这个卡顿时间中锁失效,此时ThreadB获取到锁,THreadA继续执行依旧可以释放ThreadB的锁。(极端情况)
    区分是否是当前线程的锁与删除锁不是原子性操作。且无原生命令解决。

根本点:超时时间过期问题

解决:
  1. 延长超期时间(治标不治本)
  2. 锁续命:主线程获取锁执行task任务时,分线程定时判断主线程是否执行结束,没有结束将key超时时间重新设置。

redisson 分布式锁原理:

核心特点:通过看门狗(Watchdog)自动续期机制,解决了锁超时释放与业务执行时间不匹配的难题,同时在加锁、解锁的各个环节都做了精密设计,确保原子性与安全性
如图:
在这里插入图片描述

	线程1加锁成功后默认过期时间30s,redisson启动后台线程,每隔10s检查线程1是否持有锁,持有则延长线程1持有锁时间30s。
	线程2获取所失败后堵塞,并订阅频道,在while循环中间歇性尝试加锁,当线程1释放锁后会像订阅的频道中发布消息,所有订阅该频道的线程都会被唤醒,尝试获取锁。
工作原理
  • 初始设置:客户端加锁成功时,默认设置锁的过期时间为 30秒
  • 启动续期:加锁成功后,Redisson 会为该锁启动一个看门狗线程。
  • 定时检查:该线程会每隔 10秒(即 30秒 / 3)执行一次。
  • 执行续期:每次执行时,它会检查锁是否仍被当前线程持有。如果是,它就通过 Lua 脚本将锁的过期时间重置为30秒
  • 循环往复:只要业务还在运行,锁未被主动释放,这个“检查-续期”的过程就会无限循环下去
如何停止?
  • 正常情况:当业务代码执行完毕,调用 unlock() 释放锁时,Redisson 会同时取消这个看门狗定时任务
  • 异常情况:如果持有锁的客户端宕机或网络中断,看门狗线程也随之消亡,无法进行续期。锁就会在最后一次续期后的30秒自动过期,避免了死锁
阻塞重试机制 不会消耗cpu资源

当一个客户端尝试获取锁失败时(锁已被他人持有),Redisson 并不会立刻返回失败,而是会进入一个高效的阻塞等待模式

  1. 订阅释放事件:获取锁失败的客户端会通过 Redis 的 发布/订阅(Pub/Sub) 功能,订阅一个与锁名称相关的特定频道
  2. 阻塞等待:线程会在一个 while(true) 循环中等待,并定期尝试重新获取锁
  3. 收到唤醒信号:当持有锁的客户端释放锁时,它会向这个频道发布一条释放消息
  4. 再次尝试:所有订阅了该频道的客户端会收到通知,从等待状态中被唤醒,然后再次尝试获取锁
锁的释放

unlock() 方法的实现也至关重要,核心依然是 Lua 脚本,确保操作的原子性。

  1. 检查当前线程的锁是否存在
  2. 若不存在,说明锁已失效或未被持有,抛出异常。
  3. 若存在,则将重入次数(value)减1
  4. 如果减1后的值仍然大于0,表示线程仍然持有锁(重入),只是减少计数,并重新设置过期时间。
  5. 如果减1后的值等于0,表示锁需要彻底释放,执行 del 命令删除锁的 Key,并发布一条释放锁的消息,以便通知(唤醒)其他阻塞等待的客户端。

代码如下:

 public String deductStock3() {  
    String lockKey = "lock:product_101";  
    //获取锁对象  
    RLock redissonLock = redisson.getLock(lockKey);  
    //加分布式锁  
    redissonLock.lock();  //  .setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);  
    try {  
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")  
        if (stock > 0) {  
            int realStock = stock - 1;  
            stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)  
            System.out.println("扣减成功,剩余库存:" + realStock);  
        } else {  
            System.out.println("扣减失败,库存不足");  
        }  
    } finally {  
        //解锁  
        redissonLock.unlock();  
    }  
    return "end";  
}

备注:
tryLock(long waitTIme ,long leaseTime,TImeUnit unit); 尝试加锁时间 waitTIme,加锁成功后锁过期时间 lease Time

redis主从/集群架构分布式锁失效问题:

当主节点 key 写入成功后准备同步从节点,主节点宕机,从节点成为主节点并没有此key,其他线程也可以加锁成功,针对同一个商品减库存。

redLock 红锁

红锁使用要求:使用多个相互独立的 Redis 主节点,这些节点之间没有任何复制或协调关系。只有超过半数redis节点加锁成功才算加锁成功(也没有百分百解决)

代码如下

public String redlock() {  
    String lockKey = "product\_001";  
    //这里需要自己实例化不同redis实例的redisson客户端连接,这里只是伪代码用一个redisson客户端简化了  
    RLock lock1 = redisson.getLock(lockKey);  
    RLock lock2 = redisson.getLock(lockKey);  
    RLock lock3 = redisson.getLock(lockKey);  
    //根据多个 RLock 对象构建 RedissonRedLock (最核心的差别就在这里)  
    RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);  
    try {  
        /**  
         * waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败  
         * leaseTime   锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)  
         */  
        boolean res = redLock.tryLock(10, 30, TimeUnit.SECONDS);  
        if (res) {  
            //成功获得锁,在这里处理业务  
        }  
    } catch (Exception e) {  
        throw new RuntimeException("lock fail");  
    } finally {  
        //无论如何, 最后都要解锁  
        redLock.unlock();  
    }  
    return "end";  
}

问题:依然存在主从/集群架构分布式锁失效(集群中从节点升级为主节点前未同步到主节点数据)

大促场景如何将分布式锁性能提升100倍

  1. 减小锁的粒度、
  2. 分段加锁
    如一个库存容量1000,分成10段存入,每段100份。 此时可以通过10个分布式锁操作10段数据。 代码层面维护段位库记录总标记,可以循环进行锁数据,当段数据不够一次扣减时,通过逻辑进行额外处理。
传统方式:
库存 1000 件(一个 Key) + 1 把锁 = 单点竞争
优化方式:
库存 1000 件(逻辑库存) -> 分成 10 个物理 Key(stock_01 ~ stock_10)
每个 Key 100 件,配备 1 把独立锁。
10 把锁并行工作 = 理论吞吐量提升 10 倍。

使用 Redis 的 Lua 脚本,我们可以将“判断-扣减”操作封装为原子操作,完全不需要分布式锁,从而真正将性能提升 100 倍甚至更多。
Lua 脚本在 Redis 内部执行,天然原子,且避免了锁的上下文切换开销。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐