超卖解决问题

文章介绍了乐观锁的概念和应用场景,针对库存超卖问题,从最初的乐观锁方案到一人一单策略,再到使用悲观锁避免并发下单。接着,讨论了Spring事务与synchronized关键字的配合问题,并提出了分布式锁的解决方案,包括使用Redis实现简单分布式锁以及优化锁的释放策略,以防止锁误删导致的并发问题。

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

乐观锁是什么?
乐观锁时相对于悲观锁来说,在每次提交时才会判断数据是否发生冲突。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量。
乐观锁一般采用版本号来判断数据是否发生冲突。当数据在提交时,会判断之前的版本号和提交数据时的版本号。版本号一致则提交。
库存超卖问题
seckillVoucherService为优惠券秒杀接口

//将stock设定为乐观锁的版本号,判断时如果stock一致,说明此前没有其他线程修改,当前线程进行提交。
SeckillVoucher seckillVoucher = seckillVoucherService.query().one();
boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).eq("stock", seckillVoucher.getStock()).update();

但失败率太高,当其他线程同时执行完查询库存后,在进行提交时大量线程在判断版本号时不一致,放弃提交。
该进:

SeckillVoucher seckillVoucher = seckillVoucherService.query().one();
boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).gt("stock", 0).update();

解释:当库存大于0时随便提交,直至库存为0;

一人一单
思路:用户下单,根据userId和voucherId判断记录数,记录数大于 0,说明此时该usr已下一单,禁止下单,否则就行库存扣减,创建订单

Long userId = UserHolder.getUser().getId();
Integer count = query().eq("voucher_id", voucherId).eq("user_id", userId).count();
 if (count >= 1){
     return Result.fail("请勿重复下单!");
  }
boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();
if (!success){
     Result.fail("库存不足!");
 }
//5、创建订单
.....

存在问题:多个线程同时来抢,同时判断订单不存在,则同时下单,导致多次下单。
解决:从查询订单到执行完毕加悲观锁,尽量不要加在对象上(影响性能),将锁加在用户上,多个用户加不同的锁,互不影响。
createVoucherOrder();

Long userId = UserHolder.getUser().getId();
sychronized(userId.toString().intern()){
    Integer count = query().eq("voucher_id", voucherId).eq("user_id", userId).count();
    if (count >= 1){
        return Result.fail("请勿重复下单!");
     }
    boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();
    if (!success){
         Result.fail("库存不足!");
     }
     //5、创建订单
     ........
}

存在问题:spring事务是在方法结束后提交,而sychronized执行完后事务并没有提交,此时已经释放了锁,当有其他线程进入时也会获得锁。此时的事务还没有提交。
该进:这样方法执行完事务提交后才会释放锁,然而,createVoucherOrder()是被this当前对象调用,并不是Spring管理,事务之所以生效,是被Spring对当前类做了动态代理,而this不是被代理对现象。拿到事务代理对象

sychronized(usdId){
	IVoucherOrderService proxy  = (IVoucherOrderService) AopContext.currentProxy();
    Result voucherOrder = proxy.createVoucherOrder(voucherId);
}

集群模式下,会维护不同的jvm,每个jvm会维护自己唯一一个锁监视器,单体下只有一个锁监视器,相同的userId会被锁住,集群下不同的jvm对应不同的锁监视器,同样的userId被在不同的服务器上。
在这里插入图片描述
在这里插入图片描述
解决:分布式锁
在这里插入图片描述
在这里插入图片描述
代码:定义一个锁接口和实现类

public interface ILock {
    public boolean tryLock(long timeout);
    public void unLock();
}
public class SimpleRedisLock implements ILock {
    private String name;

    public static final String KEY_PRE_FIX = "lock:";

    public static final String ID_PRE_FIX = UUID.fastUUID().toString(true) + "-";
    private StringRedisTemplate redisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate redisTemplate) {
        this.name = name;
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean tryLock(long timeout) {
        long threadId = Thread.currentThread().getId();
        String value = ID_PRE_FIX + threadId;
        Boolean isSuccess = redisTemplate.opsForValue().setIfAbsent(KEY_PRE_FIX + name, value , timeout, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(isSuccess);
    }

    @Override
    public void unLock() {
        String threadId = ID_PRE_FIX + Thread.currentThread().getId();
        String value = redisTemplate.opsForValue().get(KEY_PRE_FIX + name);
        //Todo 当前线程执行完,准备释放锁时发生了阻塞,超时释放了锁,线程2获得锁,同时当前线程阻塞结束继续执行,
        // 但却释放了线程2的锁,线程2继续执行,此时线程3发现没锁又获取了锁。
        //判断锁标识和释放锁是两个操作,不是原子操作

        if (value.equals(threadId)){
            redisTemplate.delete(KEY_PRE_FIX + name);
        }
    }
}

业务类

 long userId = UserHolder.getUser().getId();
 SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, redisTemplate);
  boolean isLock = lock.tryLock(1200);
  if (!isLock){
      return Result.fail("不允许重复下单!");
  }
  try{
      IVoucherOrderService proxy  = (IVoucherOrderService) AopContext.currentProxy();
      Result voucherOrder = proxy.createVoucherOrder(voucherId);
      return voucherOrder;
  }finally {
      lock.unLock();
  }

问题:存在锁误删,当第一个业务执行时间长或者发生阻塞时,锁超时释放,同时另一个线程执行,发现没有锁,获取锁成功,执行自己业务,线程1恢复执行,开始释放锁,然而却释放的线程2的锁,这时线程3进入,获取锁成功,此时2个线程同时操作,出现并发问题。
解决思路:释放锁时判断要释放的锁是否时自己的锁,利用UUID+线程id来确保锁标识,解决锁错误释放
在这里插入图片描述

public static final String ID_PRE_FIX = UUID.fastUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeout) {
     long threadId = Thread.currentThread().getId();
     String value = ID_PRE_FIX + threadId;
     Boolean isSuccess = redisTemplate.opsForValue().setIfAbsent(KEY_PRE_FIX + name, value , timeout, TimeUnit.SECONDS);
     return BooleanUtil.isTrue(isSuccess);
 }
@Override
public void unLock() {
    String threadId = ID_PRE_FIX + Thread.currentThread().getId();
    String value = redisTemplate.opsForValue().get(KEY_PRE_FIX + name);
    if (value.equals(threadId)){
    //Todo 当前线程执行完,准备释放锁时发生了阻塞,超时释放了锁,线程2获得锁,同时当前线程阻塞结束继续执行释放锁(释放锁之前已经判断了锁标识),
    // 但却释放了线程2的锁,线程2继续执行,此时线程3发现没锁又获取了锁。
    //判断锁标识和释放锁是两个操作,不是原子操作
        redisTemplate.delete(KEY_PRE_FIX + name);
    }
}
### 解决库存管理中问题的最佳实践 #### 使用加锁机制防止 在高并发场景下,处理问题的一个常见策略是采用加锁机制。这种机制能确保同一时间内只有一个线程能够修改库存数据,从而有效避免因多个请求同时操作而导致的商品现象[^2]。 #### 实施预扣减库存策略 针对可能出现的库存买情况,采取预先扣除一定量库存的方式也是一种有效的预防措施。当用户提交订单时不立即确认最终购买而是先预留相应数量的产品资源直到交易完成再正式减少实际可用数目;这样做可以在很大程度上降低因为瞬时大量访问造成的误判风险[^5]。 #### 利用Redis与Lua脚本优化库存控制系统 对于需要应对极高频率读写需求的应用程序来说,则可以通过引入像Redis这样的内存数据库配合特定编程语言(如Lua)编写的小型应用程序来进一步提升性能表现并增强稳定性。具体做法是在每次接收到新的购物流程请求之后立即将其加入到待处理队列当中等待执行完毕后再更新服务器端记录的状态信息以此达到同步目的同时也减少了直接对关系型数据库频繁发起查询所带来的压力[^4]。 #### 应用分布式锁技术保障一致性 考虑到现代电商网站往往部署于跨区域数据中心之上因此还需要考虑不同节点间的数据共享难题此时便可以借助诸如Zookeeper或者Redlock算法构建起一套可靠的分布式锁定协议使得即使在网络分区发生的情况下也依然维持全局唯一性的约束条件进而从根本上杜绝任何可能引起冲突的操作行为出现[^3]。 #### 维护良好的客户服务沟通渠道 除了上述技术层面的努力之外保持同消费者的良好互动同样至关重要。一旦发现问题应当迅速响应并通过适当补偿等方式尽力挽回受损的品牌形象以及客户满意度水平[^1]。 ```python import redis from threading import Lock # 创建Redis连接池实例 pool = redis.ConnectionPool(host='localhost', port=6379, decode_responses=True) def try_acquire_lock(key, timeout): r = redis.Redis(connection_pool=pool) lock = Lock() with lock: result = r.setnx(key, 'locked') if result == 1 or (result == 0 and check_expire_time(r, key)): # 设置过期时间以防止死锁 r.expire(key, timeout) return True return False def release_lock(key): r = redis.Redis(connection_pool=pool) r.delete(key) def process_order(order_id, product_id, quantity): stock_key = f'stock:{product_id}' order_key = f'order:{order_id}' while not try_acquire_lock(stock_key, 10): pass try: current_stock = int(redis.Redis(connection_pool=pool).get(stock_key)) if current_stock >= quantity: new_stock_level = str(current_stock - quantity) script = """ local current = tonumber(redis.call('GET', KEYS[1])) if current >= ARGV[1] then redis.call('DECRBY', KEYS[1], ARGV[1]) return true else return false end """ success = bool(redis.Redis(connection_pool=pool).eval(script, 1, stock_key, quantity)) if success: print(f'Order {order_id}: Successfully processed.') elif not success: raise Exception("Insufficient Stock") else: raise Exception("Not enough inventory.") finally: release_lock(stock_key) ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值