美文网首页
Redis分布式锁

Redis分布式锁

作者: packet | 来源:发表于2019-04-10 16:19 被阅读0次

    在网易的时候,用到了一种用Redis实现的分布式锁。

    tryLock(){  
        SET Key 1 EX Seconds NX
    }
    release(){  
      DELETE Key
    }
    

    但是这种实现并不严谨,有可能多个客户端同时持有一把锁。

    1. C1成功获取到了锁,之后C1因为GC进入等待或者未知原因导致任务执行过长,最后在锁失效前C1没有主动释放锁。
    2. C2在C1的锁超时后获取到锁,并且开始执行,这个时候C1和C2都同时在执行,会因重复执行造成数据不一致等未知情况。
    3. C1如果先执行完毕,则会释放C2的锁,此时可能导致另外一个C3进程获取到了锁。


    另一个版本是:

    tryLock(){  
        SET Key UniqId Seconds
    }
    release(){  
        EVAL(
          //LuaScript
          if redis.call("get",KEYS[1]) == ARGV[1] then
              return redis.call("del",KEYS[1])
          else
              return 0
          end
        )
    }
    

    UniqId是自增id,优于时间戳(可能重复)。
    这个版本避免了C1释放C2锁的情况,但依然有如下问题:

    1. 没有设置超时时间,那么持有锁的客户端crash,不能释放锁。
    2. Redis集群的数据同步为异步,假设master节点获取到了锁后,未完成数据同步的情况下crash,而此时其他客户端在新的master节点依然可以获得锁。

    在Redis分布式锁的实现上还有很多问题等待解决,及时是Redlock也遭到了强烈质疑。

    2019-04-17补充:
    上面没有给出一个正确的完整版本。

    - 获取锁(unique_value可以是UUID, requestId,自增id等)
    SET resource_name unique_value NX PX 30000
    
    public class RedisTool {
    
        private static final String LOCK_SUCCESS = "OK";
        private static final String SET_IF_NOT_EXIST = "NX";
        private static final String SET_WITH_EXPIRE_TIME = "PX";
    
        /**
         * 尝试获取分布式锁
         * @param jedis Redis客户端
         * @param lockKey 锁
         * @param requestId 请求标识
         * @param expireTime 超期时间
         * @return 是否获取成功
         */
        public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
    
            String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
    
            return LOCK_SUCCESS.equals(result);
           
        }
    }
    
    - 释放锁(lua脚本中,一定要比较value,防止误解锁)
    if redis.call("get",KEYS[1]) == ARGV[1] then
        return redis.call("del",KEYS[1])
    else
        return 0
    end
    
    
    public class RedisTool {
    
        private static final Long RELEASE_SUCCESS = 1L;
    
        /**
         * 释放分布式锁
         * @param jedis Redis客户端
         * @param lockKey 锁
         * @param requestId 请求标识
         * @return 是否释放成功
         */
        public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
    
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
            return RELEASE_SUCCESS.equals(result);
        }
    }
    

    关于Lua,它是一个封装了多个命令的脚本,里面的参数需要传递。如果调用这个脚本,要使用eval命令。
    事实上这类琐最大的缺点就是它加锁时只作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况:

    • 在Redis的master节点上拿到了锁;
    • 但是这个加锁的key还没有同步到slave节点;
    • master故障,发生故障转移,slave节点升级为master节点;
    • 其他客户端,也可以获得锁。

    鸣谢:
    一文看透 Redis 分布式锁进化史(解读 + 缺陷分析
    让面试官刮目相看的Redis分布式锁实现方式!

    相关文章

      网友评论

          本文标题:Redis分布式锁

          本文链接:https://www.haomeiwen.com/subject/wrcoiqtx.html