手撸一把分布式锁

2022-10-27 17:02:14 浏览数 (1)

1.单体架构的同步代码块

代码语言:javascript复制
public Integer prizeExchange(Long prizeId){
       Integer result = 0;
       synchronized (this) {
           result = prizeMapper.reduceStock(prizeId);
      }
       return result;
  }

使用Java提供的synchronized关键字简简单单的就为我们的奖品兑换程序添加了一把锁,同步的只有一个线程可以对我们的数据库进行减库存的操作,安全的不行,但是姜同学突然想到这个奖品兑换的服务是部署在两台服务器的是分布式的,因为synchronized是基于JAVA虚拟机的进程锁,当我们的系统变为分布式以后如果还是使用这种方式可是要出问题的哦。

2.使用redis实现分布式锁

代码语言:javascript复制
Integer result = ;
       
 Boolean lock = stringRedisTemplate.hasKey("prizeExchange");
 if (!lock) {
       // 加锁
       stringRedisTemplate.opsForValue().set("prizeExchange","lock");
       Integer stock = prizeMapper.getPrizeStock(prizeId);
       if (stock > ) {
           result = prizeMapper.reduceStock(prizeId);
      }
       // 解锁
       stringRedisTemplate.delete("prizeExchange");
  }else{
  result = ; // 用于并发而导致的兑奖失败的返回结果  
}
       
   return result;

我们的Redis无论是主从架构或者是集群架构都是独立在我们程序之外的,并且Redis是单线程的,以上特性无疑使得Redis实现分布式锁变得非常容易。但是上边的代码还是存在一点问题滴~~,如果加锁和解锁之间的代码出现了问题怎么办,比如说我们的数据库的服务器突然抽了一下筋,prizeExchange这个key是不是就永远不会消失了,所以我们要对上边的代码进行一下小小的改良。

代码语言:javascript复制
public Integer prizeExchangeUseRedis(Long prizeId){
   Integer result = ;

   Boolean lock = stringRedisTemplate.hasKey("prizeExchange");
   if (!lock) {
       // 加锁
       stringRedisTemplate.opsForValue().set("prizeExchange","lock");
       try {
           Integer stock = prizeMapper.getPrizeStock(prizeId);
           if (stock > ) {
               result = prizeMapper.reduceStock(prizeId);
          }
      } finally {
           // 解锁
           stringRedisTemplate.delete("prizeExchange");
      }

  }else{
       result = ; // 用于并发而导致的兑奖失败的返回结果
  }

   return result;
}

改良的方式也很简单就是中间减库存的业务代码成功与否我们都将prizeExchange这个Key删除掉,这样就不会造成死锁啦。但是仔细思考一下这个分布式锁还并不是很严谨。如果减完库存因为网络问题Redis突然连接不上了怎么办!!!是不是又造成死锁啦呢,所以我们还是要对上面的代码优化一下。

代码语言:javascript复制
public Integer prizeExchangeUseRedis(Long prizeId){
       Integer result = ;

       // setIfAbsent等价于Jedis中的setnx方法 为Key值设置过期时间为15s
       Boolean lock = stringRedisTemplate.opsForValue().
               setIfAbsent("prizeExchange", "lock", , TimeUnit.SECONDS);
       if (!lock) {
           // 加锁
           stringRedisTemplate.opsForValue().set("prizeExchange","lock");
           try {
               Integer stock = prizeMapper.getPrizeStock(prizeId);
               if (stock > ) {
                   result = prizeMapper.reduceStock(prizeId);
              }
          } finally {
               // 解锁
               stringRedisTemplate.delete("prizeExchange");
          }

      }else{
           result = ; // 用于并发而导致的兑奖失败的返回结果
      }

       return result;
  }

我们为prizeExchange这个Key值设置过期时间,超过15sRedis自动删除掉,因为Redis挂掉而导致的死锁的情况是不是就消失了呀。确实消失了,但是因为我们为Key值设置了过期时间,所以新的问题又产生了,如果因为网络波动,系统的延迟很高很高,20s之后问题消失了,第一个哥们刚要减库存15s到了,后面的哥们因为Key值过期所以进来了,两个人同时进来了,咱们不管这两个人是男是女,反正是要出原则性问题的。。。MD,果然想要写一段完美的程序是很难的。

代码语言:javascript复制
 public Integer prizeExchangeUseRedis(Long prizeId){
       Integer result = ;

       // setIfAbsent等价于Jedis中的setnx方法 为Key值设置过期时间为30s
       UUID uuid = new UUID(prizeId, System.currentTimeMillis());
       Boolean lock = stringRedisTemplate.opsForValue().
               setIfAbsent("prizeExchange", uuid.toString(), , TimeUnit.SECONDS);
       if (!lock) {
           // 加锁
           stringRedisTemplate.opsForValue().set("prizeExchange",uuid.toString());
           try {
               Timer timer = new Timer(true); // 开启定时器线程为当前线程的守护线程

               timer.schedule(new TimerTask() {
                   @Override
                   public void run() {
                       Boolean hasKey = stringRedisTemplate.hasKey("prizeExchange");
                       if (hasKey) {
                           stringRedisTemplate.opsForValue().
                                   set("prizeExchange",uuid.toString(),, TimeUnit.SECONDS);
                      }
                  }
              },  * 5L);
               Integer stock = prizeMapper.getPrizeStock(prizeId);
               if (stock > ) {
                   result = prizeMapper.reduceStock(prizeId);
              }
          } finally {
               // 解锁
               String prizeExchange =      stringRedisTemplate.opsForValue().get("prizeExchange");

               if (uuid.toString().equals(prizeExchange)) {
                   stringRedisTemplate.delete("prizeExchange");
              }

          }

      }else{
           result = ; // 用于并发而导致的兑奖失败的返回结果
      }

       return result;
  }

emm~,你以为这样就完了吗,好兄弟你要相信代码只要不在一行原子性就是无法保证滴,所以通过UUID判断也是有点不太可靠滴,比如执行uuid.toString().equals(prizeExchange)这句的时候判断为true,这个key刚好过期了,下一个线程拿到锁啦,stringRedisTemplate.delete(“prizeExchange”);这行代码一执行不就有乱套啦,不过没关系,Redis可以使用LUA脚本保证删除操作的一致性,而且使用起来还很简单,只需要把上边的代码解锁的部分切换成如下的代码就可以啦。

代码语言:javascript复制
 // 解锁
// 定义 lua 脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 使用 redis 执行 lua 执行
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
// 设置一下返回值类型 为 Long
// 因为删除判断的时候,返回的 0,给其封装为数据类型。如果不封装那么默认返回 String 类型,
// 那么返回字符串与 0 会有发生错误。
redisScript.setResultType(Long.class);
// 第一个要是 script 脚本 ,第二个需要判断的 key,第三个就是 key 所对应的值。
stringRedisTemplate.execute(redisScript, Collections.singletonList("prizeExchange"), uuid.toString());

总结

其实上面的代码Redisson这个框架三行API就搞定了,不过原理和我们上面的代码是差不多的,后续我会继续介绍Redisson实现Redis分布式锁的原理。

0 人点赞