您的当前位置:首页正文

分布式锁,下单,redis集群,zookeeper概念,redLock,分布式锁优化,下单基本问题解决,分布式锁面试题,下单面试题

2024-11-07 来源:个人技术集锦

读完本章将对分布式锁,下单会有新的认识。

使用setnx key value 来实现,

Setnx:不存在则设置

非常简单的分布式锁

public String deductStock(){
    String lockKey="lock:product:101";
    Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, “zyiscool“);
    if (!aBoolean){
        return "biz_code";
    }
        String stockValue = (String) stringRedisTemplate.opsForValue().get("stock");
        if (stockValue == null) {
            System.out.println("库存信息不存在");
            throw new RuntimeException("库存信息不存在无法下单");
        }
        int stock = Integer.parseInt(stockValue);
        if (stock > 0) {
            int realStock = stock - 1;
           stringRedisTemplate.opsForValue().set("stock", String.valueOf(realStock));
           System.out.println("扣减成功,剩余库存:" + realStock);
           return "ok";//返回类型根据业务自己设置
       } else {
           System.out.println("扣减失败,库存不足");
            return "error";//返回类型根据业务自己设置
       }
}

主要代码:
String lockKey= "lock:product:101";
redisTemplate.opsForValue().setIfAbsent(lockKey,"zyiscool");
redisTemplate.delete(lockKey);

这种简单的锁距离项目实际上用,还差十万八千里!

 下面问的都是上面代码的问题:

例如:问:如果中间代码抛出了异常怎么办?

     答:加finally,

     问:那如果服务崩了,还没有执行finally中的代码,你怎么办?

答:为setIfAbsent设置过期时间:

redisTemplate.opsForValue().setIfAbsent(
lockKey,
"zyiscool",
Duration.ofDays(TimeUnit.SECONDS.toSeconds(10)) 
 );

也可以这么写:

redisTemplate.opsForValue().setIfAbsent(
lockKey,
"zyiscool, 
10,
TimeUnit.SECONDS
);

重点:一定要一行完成。

完整代码:
public String deductStock(){
    String lockKey="lock:product:101";
    Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "zyiscool", 10, TimeUnit.SECONDS);
    if (!aBoolean){
        return "biz_code";
    }
    try {
        String stockValue = (String) stringRedisTemplate.opsForValue().get("stock");
        if (stockValue == null) {
            System.out.println("库存信息不存在");
            throw new RuntimeException("库存信息不存在无法下单");
        }
        int stock = Integer.parseInt(stockValue);
        if (stock > 0) {
            int realStock = stock - 1;
           stringRedisTemplate.opsForValue().set("stock", String.valueOf(realStock));
           System.out.println("扣减成功,剩余库存:" + realStock);
           return "ok";//返回类型根据业务自己设置
       } else {
           System.out.println("扣减失败,库存不足");
            return "error";//返回类型根据业务自己设置
       }
    }   catch (Exception e) {
          e.printStackTrace();
          return "error"; // 捕获异常
    }
finally {
            stringRedisTemplate.delete(lockKey);
    }

这些写其实没问题,问题不大,但是如果是高并发环境下,那么就有很多的问题:

例如:

在高并发下,设置分布式锁设置10秒钟超时。

在请求中不断有新请求对101商品上加锁。加锁被别的请求给释放掉,导致锁失效

步骤是这样的:在极端环境下,也就是高并发的环境下,被本来锁是10秒钟过期的。结果线程1运行到11秒还没有运行完,11秒时锁就已经失效了,然后线程1到15秒时,然后线程一干了一件蠢事,把锁给删掉了,这删掉的锁时线程二的,因为在线程一上锁10秒后锁过期了然后线程二就加锁成功了,线程二执行了这段代码用了8秒,重点来了,线程二执行到5秒的时候这时候锁已经被线程1给删了,然后线程二执行到8秒的时候,线程二把线程三的锁给删了。

这就导致了锁失效。导致后面一直有请求进行扣减库存,然后就会导致超卖的问题,这个bug非常大!(轻则扣工资,重则让你赔的倾家荡产(如果你有钱当我没说))

问题代码:

stringRedisTemplate.delete(lockKey);//我释放了别人的锁

解决步骤,在finally中判断是不是自己加的锁,是就删,不是就不删

解决问题后的代码:

public String deductStock(){
    String lockKey="lock:product:101";
    String clientId = UUID.randomUUID().toString();
    Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.SECONDS);
    if (!aBoolean){
        return "biz_code";
    }
    try {
        String stockValue = (String) stringRedisTemplate.opsForValue().get("stock");
        if (stockValue == null) {
            System.out.println("库存信息不存在");
            throw new RuntimeException("库存信息不存在无法下单");
        }
        int stock = Integer.parseInt(stockValue);
        if (stock > 0) {
            int realStock = stock - 1;
           stringRedisTemplate.opsForValue().set("stock", String.valueOf(realStock));
           System.out.println("扣减成功,剩余库存:" + realStock);
           return "ok";//返回类型根据业务自己设置
       } else {
           System.out.println("扣减失败,库存不足");
            return "error";//返回类型根据业务自己设置
       }
    }   catch (Exception e) {
          e.printStackTrace();
          return "error"; // 捕获异常
    }
finally {
          if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
            stringRedisTemplate.delete(lockKey);
        }
    }
}
}

继续分析代码:

下面finally代码有没有问题?

//如果是自己加的锁,那么就删除,不是就不删
      if (clientId.equals(redisTemplate.opsForValue().get(lockKey))){
           redisTemplate.delete(lockKey);
      }

有什么问题?

答:不是原子性,原子性是什么?

是代码的最小分割单位,大白话:就是要在一行代码中完成。

下面代码中:

finally {
      if (clientId.equals(redisTemplate.opsForValue().get(lockKey))){
           redisTemplate.delete(lockKey);
      }
}

在极端环境下,比如redis请求卡了,

redisTemplate.opsForValue().get(lockKey)

此时时间已经是9.900秒了,服务器网络波动导致获取锁正在查询,此时已经到9.999秒了然后查出来了,线程一接着在finally中进行比较没问题(比较完成了用了0.001秒,正好到10秒钟,然后线程二成功加上了锁),这时线程一执行删除命令。删除命令执行的时候,这下直接完蛋!这删的就是线程二加的锁,然后就引发à锁失效。然后导致超卖。(这么写的绝对是大聪明)

下面是原子性的详细介绍:

原子性:原子性是指一个操作要么完全执行,要么完全不执行,不能被中断。在这个例子中redisTemplate.opsForValue().get(lockKey) 和 redisTemplate.delete(lockKey) 这两个操作并不是原子性的。如果在获取锁的过程中,另一个线程已经成功获取了锁并执行了相关操作,那么在你删除锁时就可能会出现问题。

解决问题:

源代码:

finally {
     if (clientId.equals(redisTemplate.opsForValue().get(lockKey))){
           redisTemplate.delete(lockKey);
      }
}
//我们需要改变的代码:
if (clientId.equals(redisTemplate.opsForValue().get(lockKey))){
           redisTemplate.delete(lockKey);
      }

这两行代码如果把放一行同时执行,就是进行原子性执行。

这个我们可以使用lua脚本来执行。

下面使用lua脚本来解决问题

代码:

新建MyRedisScript类并实现RedisScript

public class MyRedisScript implements RedisScript<Long> {
    private final String script;

    public MyRedisScript(String script) {
        this.script = script;
    }

    @Override
    public String getSha1() {
        // 可以实现计算 SHA-1 的逻辑,或者返回 null
        return null;
    }

    @Override
    public Class<Long> getResultType() {
        return Long.class;
    }

    @Override
    public String getScriptAsString() {
        return this.script;
    }
}

修改方法中的判断的方法

改成这样:

String luaScript =
        "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                "   return redis.call('del', KEYS[1]) " +
                "else " +
                "   return 0 " +
                "end";

RedisScript<Long> redisScript = new MyRedisScript(luaScript);

Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(lockKey), clientId);

代码解释:

lockKey解释:这是你上的分布式锁:你上锁的时候是不是指定了商品id? 就是这个"lock:product:101"

clientId解释:你的客户端id,就是这个UUID.randomUUID().toString();要判断

Top