zoukankan      html  css  js  c++  java
  • 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)); }
  • 相关阅读:
    MFC自绘框架窗口客户区
    命令行下创建mysql数据库
    linux重置mysql root密码的6种方
    xampp修改mysql默认密码详解
    Java常用包装类
    Java异常处理
    Java数组
    Java流程控制
    Java基本数据类型
    golang https server分析
  • 原文地址:https://www.cnblogs.com/TripL/p/13346385.html
Copyright © 2011-2022 走看看