前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >可靠的分布式锁 RedLock 与 redisson 的实现

可靠的分布式锁 RedLock 与 redisson 的实现

作者头像
用户3147702
发布2022-06-27 15:55:53
4.7K0
发布2022-06-27 15:55:53
举报
文章被收录于专栏:小脑斧科技博客

1. 引言

此前的文章中,我们详细介绍了基于 redis 的分布式事务锁的实现:

厉害了,原来分布式锁有这么多坑

我们看到,分布式锁是如何一步步改进方案最终实现其高可用的。

但就“高可用”来说,似乎仍然有所欠缺,那就是如果他所依赖的 redis 是单点的,如果发生故障,则整个业务的分布式锁都将无法使用,即便是我们将单点的 redis 升级为 redis 主从模式或集群,对于固定的 key 来说,master 节点仍然是独立存在的,由于存在着主从同步的时间间隔,如果在这期间 master 节点发生故障,slaver 节点被选举为 master 节点,那么,master 节点上存储的分布式锁信息可能就会丢失,从而造成竞争条件。

那么,如何避免这种情况呢?

redis 官方给出了基于多个 redis 集群部署的高可用分布式锁解决方案 — RedLock,本文我们就来详细介绍一下。

2. RedLock 的加解锁过程

基于上述理论,我们知道,RedLock 是在多个 Redis 集群上部署的一种分布式锁的实现方式,他有效避免了单点问题。

假设我们有 N 个 Redis 服务或集群,RedLock 的加锁过程就如下所示:

  1. client 获取当前毫秒级时间戳,并设置超时时间 TTL
  2. 依次向 N 个 Redis 服务发出请求,用能够保证全局唯一的 value 申请锁 key
  3. 如果从 N/2+1 个 redis 服务中都获取锁成功,那么,本次分布式锁的获取被视为成功,否则视为获取锁失败。
  4. 如果获取锁失败,或执行达到 TTL,则向所有 Redis 服务都发出解锁请求。

3. Java 实现 — redisson

java 语言中,redisson 包实现了对 redlock 的封装,主要是通过 redis client 与 lua 脚本实现的,之所以使用 lua 脚本,是为了实现加解锁校验与执行的事务性。

此前主页君对 redis 结合 lua 实现严格的事务有过一篇文章来介绍,可以参考:

Redis 事务与 Redis Lua 脚本的编写

3.1 唯一 ID 的生成

分布式事务锁中,为了能够让作为中心节点的存储节点获悉锁的持有者,从而避免锁被非持有者误解锁,每个发起请求的 client 节点都必须具有全局唯一的 id。

通常我们是使用 UUID 来作为这个唯一 id,redisson 也是这样实现的,在此基础上,redisson 还加入了 threadid 避免了多个线程反复获取 UUID 的性能损耗:

代码语言:javascript
复制
protected final UUID id = UUID.randomUUID();
String getLockName(long threadId) {
    return id + ":" + threadId;
}

3.2 加锁逻辑

redisson 加锁的核心代码非常容易理解,通过传入 TTL 与唯一 id,实现一段时间的加锁请求。

下面是可重入锁的实现逻辑:

代码语言:javascript
复制
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    internalLockLeaseTime = unit.toMillis(leaseTime);
    // 获取锁时向5个redis实例发送的命令
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
              // 校验分布式锁的KEY是否已存在,如果不存在,那么执行hset命令(hset REDLOCK_KEY uuid+threadId 1),并通过pexpire设置失效时间(也是锁的租约时间)
              "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已存在,则校验唯一 id,如果唯一 id 匹配,表示是当前线程持有的锁,那么重入次数加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]);",
              // KEYS[1] 对应分布式锁的 key;ARGV[1] 对应 TTL;ARGV[2] 对应唯一 id
                Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

3.3 释放锁逻辑

代码语言:javascript
复制
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    // 向5个redis实例都执行如下命令
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            // 如果分布式锁 KEY 不存在,那么向 channel 发布一条消息
            "if (redis.call('exists', KEYS[1]) == 0) then " +
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; " +
            "end;" +
            // 如果分布式锁存在,但是唯一 id 不匹配,表示锁已经被占用
            "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,则删除锁,并发布解锁消息
                "redis.call('del', KEYS[1]); " +
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; "+
            "end; " +
            "return nil;",
            // KEYS[1] 表示锁的 key,KEYS[2] 表示 channel name,ARGV[1] 表示解锁消息,ARGV[2] 表示 TTL,ARGV[3] 表示唯一 id
            Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));

}

4. redisson RedLock 的使用

redisson 实现的分布式锁的使用非常简单:

代码语言:javascript
复制
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 {
    isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
    if (isLock) {
        //TODO if get lock success, do something;
    }
} catch (Exception e) {
} finally {
    redLock.unlock();
}

可以看到,由于 redisson 包的实现中,通过 lua 脚本校验了解锁时的 client 身份,所以我们无需再在 finally 中去判断是否加锁成功,也无需做额外的身份校验,可以说已经达到开箱即用的程度了。

5. redisson 的高级功能

5.1 异常情况的处理

分布式事务锁最常见的一个问题就是如果已经获取到锁的 client 在 TTL 时间内没有完成竞争资源的处理,而此时锁会被自动释放,造成竞争条件的发生。

这种情况如果让 client 端设置定时任务自动延长锁的占用时间,会造成 client 端逻辑的复杂和冗余。

redisson 在实现的过程中,自然也考虑到了这一问题,redisson 提供了一个“看门狗”的可选特性,并且增加了 lockWatchdogTimeout 配置参数,看门狗线程会自动在 lockWatchdogTimeout 超时后顺延锁的占用时间,从而避免上述问题的发生。

但是,由于看门狗作为独立线程存在,对于性能有所影响,如果并非是处理高度竞争且处理时长不固定的特殊资源,那么并不建议启用 redisson 的看门狗特性。

5.2 多个锁联合使用 — 联锁

既然 redisson 通过多个 redis 节点实现了 RedLock,那么,如果一个业务同时需要占用若干资源,是否可以将多个锁联合使用呢?答案也是可以的。

基于 Redis 的分布式 RedissonMultiLock 对象将多个 RLock 对象分组,并将它们作为一个锁处理。每个 RLock 对象可能属于不同的 Redisson 实例。

在这种复杂场景下,上述“看门狗”特性建议一定要启用,因为任何一个锁状态的崩溃都有可能会造成其他所有锁的持续挂起或资源被意外抢占。

下面是 redisson 联锁的一个示例:

代码语言:javascript
复制
public void multiLock(Integer expireTime,TimeUnit timeUnit,String ...lockKey){
    RLock [] rLocks = new RLock[lockKey.length];
    for(int i = 0,length = lockKey.length; i < length ;i ++){
      RLock lock = redissonClient.getLock(lockKey[i]);
      rLocks[i] = lock;
    }
    RedissonMultiLock multiLock = new RedissonMultiLock(rLocks);
    multiLock.lock(expireTime,timeUnit);
    logger.info("【Redisson lock】success to acquire multiLock for [ "+lockKey+" ],expire time:"+expireTime+timeUnit);
  }
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-11-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小脑斧科技博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 引言
  • 2. RedLock 的加解锁过程
  • 3. Java 实现 — redisson
    • 3.1 唯一 ID 的生成
      • 3.2 加锁逻辑
        • 3.3 释放锁逻辑
        • 4. redisson RedLock 的使用
        • 5. redisson 的高级功能
          • 5.1 异常情况的处理
            • 5.2 多个锁联合使用 — 联锁
            相关产品与服务
            云数据库 Redis
            腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档