2020-04-20 RedissonRedLock的redis分布式锁

学习资料:https://www.jianshu.com/p/f302aa345ca8

今天工作上遇到需要用分布式锁的一个问题,然后当然不是自己写一个分布式锁,公司有中间件可以直接用(封装好的),用起来分分钟,但是不知道其原理,那就不行了,所以根据源码一直走下去和看别人博客,了解了下RedissonRedLock的原理。

查看人家的源码,学习别人的写法是很有必要的。公司的源码就不贴出来了。记录一下RedissonRedLock整个流程吧

1,初始化链接RedissonClient

要使用RedissonRedLock,必须先有redis链接,RedissonClient这个类就是初始化redis链接的。初始化RedissonClient就需要地址密码数据库必要条件啦,通过Config来创建RedissonClient

Set<RedissonClient> clients = new HashSet<>();
for (LockDTO lock : locks) {
    Config config = new Config();
    SingleServerConfig singleServerConfig = config.useSingleServer();
             singleServerConfig.setAddress(lock.getAddress()).setDatabase(lock.getDb());
    if (StringUtils.isNotBlank(lock.getPassword())) {
           singleServerConfig.setPassword(lock.getPassword());
    }
    //执行create创建链接,这里链接放在set里,可以创建多个
    clients.add(Redisson.create(config));
}




class LockDTO {
    String address;
    String password;
    int db = 0;
}

2,新建RedissonRedLock

因为tryLock和unLock是RedissonRedLock的方法,所以new出RedissonRedLock的对象

//RedissonRedLock构造方法
 public RedissonRedLock(RLock... locks) {
        super(locks);
 } 
//根据构造方法来
List<RLock> rLockList = new ArrayList<>();
 for (RedissonClient client : clients) {
    rLockList.add(client.getLock(key));
 }
//
RedissonRedLock redLock = new RedissonRedLock(rLockList.toArray(转数组));

3,尝试获取锁,tryLock其实是RedissonMultiLock的方法

入参数:waitTime:最多等待时间,leaseTime锁过期时间,unit 单位

我的版本是2.15.1,学习博客的是2.14的,源码有不同,基本类似。

public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        long newLeaseTime = -1;
        if (leaseTime != -1) {
            newLeaseTime = unit.toMillis(waitTime)*2;
        }
        
        long time = System.currentTimeMillis();
        long remainTime = -1;
        if (waitTime != -1) {
            remainTime = unit.toMillis(waitTime);
        }
        // 等待时间
        long lockWaitTime = calcLockWaitTime(remainTime);
        //加锁失败时间限制
        int failedLocksLimit = failedLocksLimit();
        List<RLock> acquiredLocks = new ArrayList<RLock>(locks.size());
        //遍历RLock锁
        for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
            RLock lock = iterator.next();
            boolean lockAcquired;
            try {
                //没有等待时间或者失效时间就直接获取锁
                if (waitTime == -1 && leaseTime == -1) {
                    //最底层的获取锁
                    lockAcquired = lock.tryLock();
                } else {
                    //根据等待时间和失效时间去获取锁
                    long awaitTime = Math.min(lockWaitTime, remainTime);
                    //最底层的获取锁
                    lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
                }
            } catch (RedisResponseTimeoutException e) {
                //异常了需要解锁
                unlockInner(Arrays.asList(lock));
                lockAcquired = false;
            } catch (Exception e) {
                lockAcquired = false;
            }
            
            if (lockAcquired) {
                //获取成功了就加入集合
                acquiredLocks.add(lock);
            } else {
                //达到了失败限制就退出循环,不获取锁
                if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
                    break;
                }
                //失效限制为0就需要把锁解除
                //底层有用到AtomicInteger
                if (failedLocksLimit == 0) {
                    unlockInner(acquiredLocks);
                    if (waitTime == -1 && leaseTime == -1) {
                        return false;
                    }
                    failedLocksLimit = failedLocksLimit();
                    acquiredLocks.clear();
                    // reset iterator
                    while (iterator.hasPrevious()) {
                        iterator.previous();
                    }
                } else {
                    failedLocksLimit--;
                }
            }
            //等待时间判断,超过等待时间就解锁
            if (remainTime != -1) {
                remainTime -= (System.currentTimeMillis() - time);
                time = System.currentTimeMillis();
                if (remainTime <= 0) {
                    unlockInner(acquiredLocks);
                    return false;
                }
            }
        }
        
        if (leaseTime != -1) {
            List<RFuture<Boolean>> futures = new ArrayList<RFuture<Boolean>>(acquiredLocks.size());
            for (RLock rLock : acquiredLocks) {
                RFuture<Boolean> future = rLock.expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS);
                futures.add(future);
            }
            
            for (RFuture<Boolean> rFuture : futures) {
                rFuture.syncUninterruptibly();
            }
        }
        
        return true;
    }

lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);这步是最底层的方法,一直深入进去可以看到如下代码:

下面的字符串就是redis命令。有三个参数

  • KEYS[1]就是Collections.singletonList(getName()),表示分布式锁的key,即REDLOCK_KEY;

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

  • ARGV[2]就是getLockName(threadId),是获取锁时set的唯一值,即UUID+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,
                  "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; " +
                  "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; " +
                  "return redis.call('pttl', KEYS[1]);",
                    Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
    }

4,释放锁

@Override
    public void unlock() {
        unlockInner(locks);
    }

5,用法

//准备
List<RLock> rLockList = new ArrayList<>();
 for (RedissonClient client : clients) {
    rLockList.add(client.getLock(key));
 }
RedissonRedLock redLock = new RedissonRedLock(rLockList.toArray(转数组));

//获取锁
redLock.tryLock(waitTime, leaseTime, unit);
//业务逻辑
try{
  //你的业务逻辑


} catch () {
  
} finally {
    //保证一定会释放锁
  redLock.unlock(key)
}

6,实现原理

https://mp.weixin.qq.com/s/JLEzNqQsx-Lec03eAsXFOQ

 

antirez提出的redlock算法大概是这样的:

在Redis的分布式环境中,我们假设有N个Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。我们确保将在N个实例上使用与在Redis单实例下相同方法获取和释放锁。现在我们假设有5个Redis master节点,同时我们需要在5台服务器上面运行这些Redis实例,这样保证他们不会同时都宕掉。

为了取到锁,客户端应该执行以下操作:

  • 获取当前Unix时间,以毫秒为单位。

  • 依次尝试从5个实例,使用相同的key和具有唯一性的value(例如UUID)获取锁。当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试去另外一个Redis实例请求获取锁。

  • 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(N/2+1,这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功

  • 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。

  • 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)

公司只用了3个redis,可重入锁获取2个及以上就说明获取锁成功

<think>好的,我现在要解决用户的问题:如何利用Redis的WAIT命令配合分布式锁来避免主从切换时的问题。首先,我需要理解用户提到的具体问题是什么。根据引用[1]里的描述,当主节点挂了,切换到从节点时,如果主节点的锁信息还没同步到从节点,其他线程可能再次获取锁,导致锁失效。这时候用户想知道如何用WAIT命令强制同步所有从节点来避免这个问题。 首先,我得回顾一下Redis的主从复制机制。默认情况下,主节点的写操作是异步复制到从节点的,这可能导致主节点宕机后数据丢失。WAIT命令的作用是让主节点在写操作后,等待指定数量的从节点确认接收并处理该命令,这样可以增强数据的一致性。不过,WAIT命令只能保证数据被复制到从节点的内存,并不保证持久化到磁盘,但在这个场景中,内存复制可能已经足够。 接下来,我需要考虑如何在加锁过程中使用WAIT命令。用户可能想的是,在获取锁之后,执行WAIT命令,强制主节点等待所有从节点同步完成,然后再认为锁获取成功。这样即使主节点宕机,切换后的新主节点也会有锁的信息,避免其他客户端再次获取锁。 但是,这里有几个需要注意的地方。首先,使用WAIT命令会增加延迟,因为必须等待所有从节点确认。这会影响到加锁的性能,尤其是在从节点较多或网络延迟较高的情况下。其次,WAIT命令的可靠性取决于从节点的数量和配置,如果设置的从节点数不够,或者部分从节点不可用,可能导致WAIT命令阻塞或失败。 然后,我需要参考Redisson的实现。根据引用[3],Redisson在加锁时默认只会同步到过半的从节点。用户可能希望修改这个配置,使其同步到所有从节点,或者结合WAIT命令来确保同步完成。不过,Redisson可能已经有相关的配置选项,或者需要自定义脚本来实现。 另外,引用[2]提到分布式锁用于任务调度,避免重复执行任务,所以锁的可靠性至关重要。如果主从切换导致锁失效,可能导致任务重复执行,产生错误。因此,使用WAIT命令来增强数据一致性是有必要的,但需要权衡性能和可靠性。 可能的问题点包括:如何确定WAIT命令的参数(如等待的从节点数量和超时时间),如何处理WAIT命令执行失败的情况,以及如何与现有的锁机制(如Redisson的RLock)结合使用。此外,还需要考虑Redis集群模式下的情况,因为WAIT命令在集群中的行为可能不同。 总结来说,解决方案的大致步骤应该是:在加锁成功后,执行WAIT命令等待指定数量的从节点同步,然后再返回加锁成功。这样可以确保锁信息已经同步到足够多的从节点,减少主从切换后锁丢失的风险。但需要根据实际环境调整参数,并评估对性能的影响。</think>### 通过Redis分布式锁结合`WAIT`命令解决主从同步问题 在Redis主从架构中,主节点异步复制数据到从节点。若主节点在加锁后宕机且锁信息未同步到从节点,切换新主节点时可能导致锁失效,其他线程可能重复获取锁。`WAIT`命令可以强制主节点等待数据同步到指定数量的从节点,增强一致性。以下是实现方法: --- #### 1. **`WAIT`命令的作用** - **功能**:`WAIT <numreplicas> <timeout>`命令使主节点阻塞,直到当前写操作同步到至少`numreplicas`个从节点,或超时(毫秒)[^1]。 - **示例**:`WAIT 2 5000`表示等待至少2个从节点同步,最多等待5秒。 --- #### 2. **结合分布式锁的实现步骤** 1. **加锁时强制同步**: 使用`SET`命令设置锁,并立即调用`WAIT`确保同步: ```bash SET lock_key unique_value NX EX 30 # 设置锁,30秒过期 WAIT 3 1000 # 等待至少3个从节点同步,超时1秒 ``` - **风险**:若同步超时或节点不足,需回滚锁(删除`lock_key`)并重试。 2. **在Redisson中集成**: Redisson默认异步同步锁到从节点。可通过自定义脚本强制同步: ```java RLock lock = redissonClient.getLock("myLock"); lock.lock(); // 执行WAIT命令(需通过eval脚本调用) String script = "redis.call('WAIT', 3, 1000); return 1;"; redissonClient.getScript().eval(RScript.Mode.READ_WRITE, script, RScript.ReturnType.VALUE); ``` 需权衡性能与可靠性[^3]。 --- #### 3. **注意事项** - **性能影响**:同步等待会增加加锁延迟,需根据业务容忍度调整`numreplicas`和`timeout`。 - **集群模式**:`WAIT`仅对当前主节点连接的从节点生效,集群模式下需确保节点归属。 - **超时处理**:若`WAIT`超时,需判定锁是否有效(可能部分从节点未同步),建议结合过期时间自动释放。 --- #### 4. **替代方案:RedLock算法** 若`WAIT`实现复杂,可改用Redis官方推荐的**RedLock算法**,通过多主节点投票加锁,降低单点依赖: ```java RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3); redLock.lock(); try { // 业务逻辑 } finally { redLock.unlock(); } ``` 但RedLock需部署多个独立Redis实例,成本较高[^1]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值