多节点并发状态下导致商品超卖问题的渐进解决方案

1.没有锁的情况

假设库存只剩1件产品,且物品不允许超卖。

扣减库存:

@GetMapping("/reduce")

public void koujiankucun(String itemName) {

1.    Integer count = Integer.parseInt(redisTemplate.opsForValue().get(itemName)) //查看该物品库存数量

2.    if (count > 0)       //如果数量大于0,代表可以进行库存扣减

3.   redisTemplate.opsForValue().set(itemName, String.valueof(count - 1));  
         //库存扣减写回redis

}

此时会出现问题:假设线程A拿到了count的值为1,接着执行到语句2,线程B执行语句1,线程A执行到语句3之前,线程B已经拿到了count的值也为1,于是线程A与线程B都会进行库存扣减,导致库存的值为-1。(即同一个jvm多线程下都无法保证安全)

2.加入synchronized锁

假设加入了synchronized锁代码如下所示:

public void koujiankucun(String itemName) {
    synchronized (this) {
    1.  Integer count = Integer.parseInt(redisTemplate.opsForValue().get(itemName))//查看该物品库存数量
        
    2.  if (count > 0)       //如果数量大于0,代表可以进行库存扣减

    3.  redisTemplate.opsForValue().set(itemName, String.valueof(count - 1));//库存扣减写回redis
    }
}

此时通过synchronized锁可以锁住当前对象,即可保证在同一jvm下多线程并发问题。

但是在多节点情况下依然存在问题:在多节点情况下,可能有多个服务器部署了这个服务,如果用户将请求发给了多个服务器,但是每个服务器只能锁住自己服务器的那个对象。因此多个服务器也会出现多次扣减库存的情况。(即在多个jvm下依然会出现并发问题)

3.加入分布式锁(通过setnx)

redis的setnx命令:向redis中添加一个key,只用当key不存在的时候才添加并返回1,存在则不添加返回0。命令是原子性的。springboot中调用setIfAbsent方法就行(底层是setnx)。

假设加入了分布式锁代码如下所示:

public void koujiankucun(String itemName) {

1.    Boolean islock = redisTemplate.opsForValue().setIfAbsent(lock, "1");

      if (islock) {
2.       redisTemplate.expire(lock, 5, TimeUnit.SECONDS);//设置key的过期时间,否则的话可能出现锁死情况

        if (count > 0)       //如果数量大于0,代表可以进行库存扣减

        redisTemplate.opsForValue().set(itemName, String.valueof(count - 1));//库存扣减写回redis
    }
}

此解决方式仍然存在一定问题:分布式锁和过期时间二者加起来并不是一个原子操作(即语句1和语句2结合起来看并不是一个原子操作)。它会出现问题。例如:线程A执行完语句1,还未执行语句2,结果线程A挂掉了。此时因为线程A还没有给锁设置过期时间,那么其他线程将一直阻塞等待该锁的释放。

4.使分布式锁和过期时间原子性化

2.6以前:使用lua脚本

public void koujiankucun(String itemName) {

String locklua = "" + "if redis.call('setnx',KEYS[1],ARVG[1])==1 then 

redis.call('expire',KEYS[1],ARVG[2]);" +
        "return true" + "else return false" + "end";    //Lua脚本

Boolean islock = redisTemplate.execute(new RedisCallback<Boolean>() {
    
@Override
    
public Boolean doInRedis(RedisConnection redisConnection) throws DataAccessException {
        Boolean eval = redisConnection.eval(
                locklua.getBytes(),
                ReturnType.BOOLEAN,
                1,
                lock.getBytes(),
                "1".getBytes(),
                "5".getBytes()
        );
        
redisConnection.setNX(lock.getBytes(), "1".getBytes());
        return null;
    }

});
if (islock) {
    redisTemplate.expire(lock, 5, TimeUnit.SECONDS);//设置key的过期时间,否则的话可能出现锁死情况

    if (count > 0)       //如果数量大于0,代表可以进行库存扣减

        redisTemplate.opsForValue().set(itemName, String.valueof(count - 1));//库存扣减写回redis
}
}

2.6以后:使用set key value nx ex

综合上述两种方式,此时仍然可能存在问题:即业务超时导致错误解锁问题:

例如,线程A设置锁过期时间为10s.假设线程A的业务逻辑处理需要30s。

线程A加锁,执行业务逻辑,但是因为锁的过期时间小于业务超时时间,即线程A业务处理了仅10s,锁超时释放,但是A全然未知,继续执行业务。此时线程B趁机申请到了锁,且线程B设置了超时时间为30s,线程B执行自己的业务逻辑。此时在20s之内,线程A执行了自己的业务,线程B也执行了自己的业务,二者可能出现同时操纵共享资源导致程序运行出现问题。这是第一个问题。

第二个可能出现的问题是:在接下来线程A执行完自己的业务后,线程A通过del lock手动删除锁,但是这个锁并不是自己一开始加的锁了,而是上述线程B所施加的锁。于是线程B执行了自己的30s之后,发现自己的锁神秘的消失了。

综上所述,目前还存在两大问题:1.业务超时锁释放导致业务并发执行。2.业务超时导致错误解锁。

5.解决4中的第二个问题

关注 Boolean islock = redisTemplate.opsForValue().setIfAbsent(lock, "1")语句:

我们可以将lock的value值更改为一个requestId(采用UUID+currentThread的id的形式),这样的话在释放锁的阶段可以先判断是不是自己的锁,如果是自己的锁才能释放。代码如下:

public void koujiankucun(String itemName) {
    String requestId = UUID.randomUUID() + currentThread().getId();

    Boolean islock = redisTemplate.opsForValue().setIfAbsent(lock, requestId);

    try {
        if (islock) {
            redisTemplate.expire(lock, 5, TimeUnit.SECONDS);//设置key的过期时间,否则的话可能出现锁死情况

            if (count > 0)       //如果数量大于0,代表可以进行库存扣减

            redisTemplate.opsForValue().set(itemName, String.valueof(count - 1));//库存扣减写回redis
        }
    } finally {
   1.     String id = redisTemplate.opsForValue().get(lock);
        //判断是自己的requestId才进行主动释放锁
   2.       if (id != null && id.equals(requestid))
   3.        redisTemplate.delete(lock);
    }
}

但是语句1和2综合起来看并不是原子操作,仍然存在线程A执行1,2的时候锁超时,从而线程B乘虚而入,获取锁,然后线程A执行语句3,即释放了不属于自己的那个锁(线程B)的锁。按照4中lua脚本的经验,可以使用lua脚本将语句1,2,3打包成原子操作。即可解决4中第二个问题。

6.解决4中的第一个问题

使用锁续期方案:

例如,设置锁过期时间为10s.假设线程A的业务逻辑处理需要30s。

在线程A设置锁过期时间为10s的时候,创建守护线程。守护线程做的任务是:设置定时任务,每5s检查A是否持有锁,如果持有锁,证明A的业务未能做完,那么守护线程会给A增加过期时间。如此循环往复,直到线程A执行完业务锁释放,守护线程关闭。

7.使用redission可以解决上述所有问题

redission底层使用了上述的所有思想。因此直接使用redissonClient.getLock()方法获得RLock对象。调用lock方法加锁,后面写加锁的内容,最后调用unlock方法解锁就可以保证在多节点下的商品超卖问题。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值