Redis分布式锁

1、单机版分布式锁

SET key value[EX seconds][PX milliseconds][NX|XX]

 

key 标志位
value 唯一值,自己只能释放自己的锁
EX seconds 设置过期时间,单位为秒
PX milliseconds 设置过期时间,单位毫秒
NX 仅当key不存在时设置值
XX 仅当key存在设置值

  

    public boolean tryLock(String key, String uniqueId, int seconds) {
        SetParams setParams = new SetParams();
        setParams.ex(seconds);
        setParams.nx();
        return "OK".equals(jedis.set(key, uniqueId, setParams));
    }

uniqueId必须为唯一,解决的场景是:

  • 客户端A获取锁成功
  • 客户端A业务逻辑执行时间>失效时间,锁被释放
  • 客户端B获取锁成功
  • 客户端A业务执行完毕,释放掉B的锁。
  • 加上唯一value之后,删除锁先比较value防止删除错误锁
    public boolean deleteLock(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";
        return jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(value)).equals(1L);
    }

2、问题

  这类缺点是加锁操作只作用在一个redis节点上,即使通过sentinel保证高可用,还是可能出现锁丢失,如以下情况:

  • 客户端在Redis的master获取锁成功
  • 加锁的key还未同步到salve
  • master failover
  • salve节点升级为master
  • 锁丢失,多个客户端获取到锁

3、RedLock

  • 有N个Redis Master,这些节点完成独立,我们在N个Master上使用相同方法获取锁和释放锁。
  • 假设有五个Redis Master节点,我们需要在五台服务器上分别运行,可以保证他们不会同时宕机
  • 为了获取锁,客户端要执行以下操作:
    • 获取当前Unix时间,以毫秒为单位
    • 依次尝试从5个实例,使用相同的key和具有唯一性的value来获取锁。
    • 客户端设置一个获取锁的超时时间,该超时时间应该远小于key的失效时间,如果超过获取锁时间,那么马上向下一个Redis Mater获取锁
    • 客户端使用当前时间 - 第一步获取的时间,就是获取锁消耗的时间
    • 如果5/2+1的节点都获取到锁,并且获取锁消耗的时间小于key的失效时间,那么锁获取成功
    • key的真正有效时间 = 失效时间 - 获取锁的时间
    • 如果因为某些原因,获取锁失败,客户端向在所有Redis Master上进行解锁

3.1、Redisson RedLock

redisson已经有对redlock算法封装

    public void tryLock() {
        Config config = new Config();
        config.useSentinelServers()
                .addSentinelAddress("127.0.0.1:6369", "127.0.0.1:6379", "127.0.0.1:6389")
                .setMasterName("masterName")
                .setPassword("password")
                .setDatabase(0);
        RedissonClient redissonClient = Redisson.create(config);
        RLock redLock = redissonClient.getLock("REDLOCK_KEY");
        boolean isLock;
        try {
            //500ms获取锁的超时时间,10000ms是锁失效时间
            isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
        } catch (Exception e) {

        } finally {
            redLock.unlock();
        }
    }

实现分布式锁的一个非常重要的点就是set的value要具有唯一性,redisson的value是怎样保证value的唯一性呢?答案是UUID+threadId

    protected String getLockName(long threadId) {
        return id + ":" + threadId;
    }

获取锁

    <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
          //首先分布式锁key不存在,执行hset命令,并设置过期时间
"if (redis.call('exists', KEYS[1]) == 0) then " + "redis.call('hset', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " +
          //如果key存在,并且value匹配,那么重入次数+1,并设置失效时间 "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + "redis.call('hincrby', KEYS[1], ARGV[2], 1); " + "redis.call('pexpire', KEYS[1], ARGV[1]); " + "return nil; " + "end; " +
          // 获取分布式锁的KEY的失效时间毫秒数 "return redis.call('pttl', KEYS[1]);", Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId)); }
  • KEYS[1]就是Collections.singletonList(getName()),表示分布式锁的key,即REDLOCK_KEY;

  • ARGV[1]就是internalLockLeaseTime,即锁的租约时间,默认30s;

  • ARGV[2]就是getLockName(threadId),是获取锁时set的唯一值,即UUID+threadId:

 

释放锁

    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
        // 如果分布式锁存在,但是value不匹配,表示锁已经被占用,那么直接返回
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " + "return nil;" + "end; " +
        // 如果就是当前线程占有分布式锁,那么将重入次数减1 "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
        //重入次数减1后的值如果大于0,表示分布式锁有重入过,那么只设置失效时间,还不能删除 "if (counter > 0) then " + "redis.call('pexpire', KEYS[1], ARGV[2]); " + "return 0; " + "else " +
        //重入次数减1后的值如果为0,表示分布式锁只获取过1次,那么删除这个KEY,并发布解锁消息 "redis.call('del', KEYS[1]); " + "redis.call('publish', KEYS[2], ARGV[1]); " + "return 1; "+ "end; " + "return nil;", Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId)); }

 

posted @ 2020-07-20 17:45  TPL  阅读(144)  评论(0编辑  收藏  举报