超卖解决问题

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

乐观锁是什么?
乐观锁时相对于悲观锁来说,在每次提交时才会判断数据是否发生冲突。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量。
乐观锁一般采用版本号来判断数据是否发生冲突。当数据在提交时,会判断之前的版本号和提交数据时的版本号。版本号一致则提交。
库存超卖问题
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);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值