美文网首页
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