什么是分布式锁

834 阅读4分钟

原文:chenmingyu.top/redis-lock/

分布式锁

  分布式锁其实可以理解为:控制分布式系统有序的去对共享资源进行操作,通过互斥来保持一致性。 举个不太恰当的例子:假设共享的资源就是一个房子,里面有各种书,分布式系统就是要进屋看书的人,分布式锁就是保证这个房子只有一个门并且一次只有一个人可以进,而且门只有一把钥匙。然后许多人要去看书,可以,排队,第一个人拿着钥匙把门打开进屋看书并且把门锁上,然后第二个人没有钥匙,那就等着,等第一个出来,然后你在拿着钥匙进去,然后就是以此类推

实现原理

  1. 互斥性

    保证同一时间只有一个客户端可以拿到锁,也就是可以对共享资源进行操作

  2. 安全性

    只有加锁的服务才能有解锁权限,也就是不能让a加的锁,bcd都可以解锁,如果都能解锁那分布式锁就没啥意义了

    可能出现的情况就是a去查询发现持有锁,就在准备解锁,这时候忽然a持有的锁过期了,然后b去获得锁,因为a锁过期,b拿到锁,这时候a继续执行第二步进行解锁如果不加校验,就将b持有的锁就给删除了

  3. 避免死锁

    出现死锁就会导致后续的任何服务都拿不到锁,不能再对共享资源进行任何操作了

  4. 保证加锁与解锁操作是原子性操作

    例:

    假设a用redis实现分布式锁,

    1,设置key set(key,value)

    2,给key设置过期时间

    假设现在a刚实现set后,程序崩了就导致了没给key设置过期时间就导致key一直存在就发生了死锁

如何实现分布式锁

  实现分布式锁的方式有很多,只要满足上述条件的都可以实现分布式锁,比如数据库,redis,zookeeper,在这里就先讲一下如何使用redis实现分布式锁

使用redis实现分布式锁

步骤核心:

  1. 使用redis命令 set key value NX EX max-lock-time实现加锁
  2. 使用redis命令 EVAL 实现解锁

需要满足分布式锁的实现原理

加锁
   Jedis jedis = new Jedis("127.0.0.1", 6379);
   private static final String SUCCESS = "OK";

   /**
    * 加锁操作
    * @param key 锁标识
    * @param value 客户端标识
    * @param timeOut 过期时间
    */
   public Boolean lock(String key,String value,Long timeOut){
       String var1 = jedis.set(key,value,"NX","EX",timeOut);
       if(LOCK_SUCCESS.equals(var1)){
           return true;
       }
       return false;
   }

解读

  1. 加锁操作:jedis.set(key,value,"NX","EX",timeOut),保证加锁的原子操作

  2. key就是rediskey值作为锁的标识,value在这里作为客户端的标识,只有key-value都比配才有删除锁的权利,保证安全性

  3. 通过timeOut设置过期时间保证不会出现死锁,避免死锁

  4. NXEX什么意思

    NX:只有这个key不存才的时候才会进行操作,if not exists

    EX:设置key的过期时间为秒,具体时间由第5个参数决定

解锁
   Jedis jedis = new Jedis("127.0.0.1", 6379);
   private static final Long UNLOCK_SUCCESS = 1L;

   /**
    * 解锁操作
    * @param key 锁标识
    * @param value 客户端标识
    * @return
    */
   public static Boolean unLock(String key,String value){
       String luaScript = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else  return 0 end";
       Object var2 = jedis.eval(luaScript,Collections.singletonList(key), Collections.singletonList(value));
       if (UNLOCK_SUCCESS == var2) {
           return true;
       }
       return false;
   }

解读

  1. luaScript 这个字符串是个lua脚本,代表的意思是如果根据key拿到的value跟传入的value相同就执行del,否则就返回0,保证安全性
  2. jedis.eval(String,list,list);这个命令就是去执行lua脚本,KEYS的集合就是第二个参数,ARGV的集合就是第三参数【保证解锁的原子操作】

​ 上述就实现了怎么使用redis去正确的实现分布式锁,但是有个小缺陷就是锁过期时间要设置为多少合适,这个其实还是需要去根据业务场景考量一下的

重试机制

  上面那只是讲了加锁与解锁的操作,试想一下如果在业务中去拿锁如果没有拿到是应该阻塞着一直等待还是直接返回,这个问题其实可以写一个重试机制,根据重试次数和重试时间做一个循环去拿锁,当然这个重试的次数和时间设多少合适,是需要根据自身业务去衡量的

    /**
    * 重试机制
    * @param key 锁标识
    * @param value 客户端标识
    * @param timeOut 过期时间
    * @param retry 重试次数 不要太大
    * @param sleepTime 重试间隔时间
    * @return
    */
   public Boolean lockRetry(String key,String value,Long timeOut,Integer retry,Long sleepTime){
       Boolean flag = false;
       try {
           for (int i=0;i<retry;i++){
               flag = lock(key,value,timeOut);
               if(flag){
                   break;
               }
               Thread.sleep(sleepTime);
           }
       }catch (Exception e){
           e.printStackTrace();
       }
       return flag;
   }

redis实现分布式锁就写完了

文中set命令详解:redisdoc.com/string/set.…