Redis进阶- Redisson分布式锁实现原理及源码解析

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
云解析 DNS,旗舰版 1个月
简介: Redis进阶- Redisson分布式锁实现原理及源码解析


Pre

Redis进阶-细说分布式锁中我们梳理了使用Redis实现分布式锁的演进过程,并提出了目前最完善的解决方案:Redisson 实现分布式锁 。

这里我们来分析下Redisson分布式锁实现原理及源码解析


用法

使用redisson实现分布式锁的操作步骤,三部曲

  • 第一步: 获取锁 RLock redissonLock = redisson.getLock(lockKey);
  • 第二步: 加锁,实现锁续命功能 redissonLock.lock();
  • 第三步:释放锁 redissonLock.unlock();

Redisson分布式锁实现原理

熟悉了基本用法以后,我们来看下Redission实现分布式锁的原理,再理解了原理之后,后续梳理源码实现就更加得心应手了。


Redisson分布式锁源码分析

流程图如下

重点主要是依赖lua脚本的原子性,实现加锁和释放锁的功能

redisson.getLock(lockKey) 的逻辑

@Override
    public RLock getLock(String name) {
        return new RedissonLock(connectionManager.getCommandExecutor(), name);
    }

实例化RedissonLock,我们看下RedissonLock的构造函数

public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
        super(commandExecutor, name);
        this.commandExecutor = commandExecutor;
        this.id = commandExecutor.getConnectionManager().getId();
        this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
    }
  • super(commandExecutor, name); 父类name赋值,后续通过getName()获取
  • commandExecutor: 执行lua脚本的executor
  • id 是个UUID, 后面被用来当做 和threadId组成 value值,用作判断加锁和释放锁是否是同一个线程的校验。
  • internalLockLeaseTime : 取自 Config#lockWatchdogTimeout,默认30秒,这个参数还有另外一个作用,锁续命的执行周期 internalLockLeaseTime/3 = 10秒

redissonLock.lock()的逻辑

主要是实现加锁和锁的续命

redissonLock.lock();

看看都干了啥

@Override
    public void lock() {
        try {
            lockInterruptibly();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

继续看 lockInterruptibly

@Override
    public void lockInterruptibly() throws InterruptedException {
        lockInterruptibly(-1, null);
    }

继续看 lockInterruptibly(-1, null);

@Override
    public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
      // 获取当前线程ID
        long threadId = Thread.currentThread().getId();
        // 尝试获取锁的剩余时间 
        Long ttl = tryAcquire(leaseTime, unit, threadId);
        // lock acquired  ttl为空,说明没有线程持有该锁,直接返回 让当前线程加锁成功 
        if (ttl == null) {
            return;
        }
        RFuture<RedissonLockEntry> future = subscribe(threadId);
        commandExecutor.syncSubscription(future);
        // 死循环  
        try {
            while (true) {
                // 再此尝试获取锁的剩余时间 ,如果为null, 跳出循环
                ttl = tryAcquire(leaseTime, unit, threadId);
                // lock acquired
                if (ttl == null) {
                    break;
                }
                // waiting for message   如果ttl >=0 说明 有其他线程持有该锁
                if (ttl >= 0) {
                     // 获取信号量,尝试加锁,设置最大等待市场为ttl
                    getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                     // 如果ttl小于0 (-1 ,-2 ) 说明已经过期,直接获取
                    getEntry(threadId).getLatch().acquire();
                }
            }
        } finally {
            unsubscribe(future, threadId);
        }
//        get(lockAsync(leaseTime, unit));
    }

大流程已经梳理完了,我们看下 Long ttl = tryAcquire(leaseTime, unit, threadId);

private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
        return get(tryAcquireAsync(leaseTime, unit, threadId));
    }

继续看下

tryAcquireAsync(leaseTime, unit, threadId)
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
        if (leaseTime != -1) {
            return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        }
        // 刚开始  leaseTime 传入的是 -1 ,所以走这个分支
        // 1)尝试加锁  待会细看 先把主要的逻辑梳理完
        RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
       // 2) 注册监听事件
        ttlRemainingFuture.addListener(new FutureListener<Long>() {
            @Override
            public void operationComplete(Future<Long> future) throws Exception {
                if (!future.isSuccess()) {
                    return;
                }
                Long ttlRemaining = future.getNow();
                // lock acquired
                if (ttlRemaining == null) {
                  // 3)获取锁成功的话,给锁延长过期时间 
                    scheduleExpirationRenewal(threadId);
                }
            }
        });
        return ttlRemainingFuture;
    }

继续看

// 1)尝试加锁  待会细看 先把主要的逻辑梳理完
        RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);

看实现

<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));
    }

lua 脚本

KEYS[1] ---------> getName()

ARGV[1] ---------> internalLockLeaseTime

ARGV[2] ---------> getLockName(threadId) 实现如下

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

这个id就是自开始实例化RedissonLock的id ,是个UUID

我们来解释下这段lua脚本

// 如果 lockKey不存在 ,设置 使用hset设置 lockKey ,field为 uuid:threadId ,value为1 ,并设置过期时间
 //就是这个命令 
 //127.0.0.1:6379> hset lockkey  uuid:threadId 1
 //(integer) 1
 //127.0.0.1:6379> PEXPIRE lockkey internalLockLeaseTime
  "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; " +
 // 如果 lockKey 存在和 filed 和 当前线程的uuid:threadId相同  key 加1 ,执行多少次 就加多次  设置过期时间  其实就是如下命令
 //127.0.0.1:6379> HEXISTS lockkey uuid:threadId
 //(integer) 1
 //127.0.0.1:6379> PEXPIRE lockkey  internalLockLeaseTime
   "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; " +
 // 最后返回 lockkey的 pttl 
   "return redis.call('pttl', KEYS[1]);"

那继续监听时间中的 scheduleExpirationRenewal(threadId); 逻辑

private void scheduleExpirationRenewal(final long threadId) {
        if (expirationRenewalMap.containsKey(getEntryName())) {
            return;
        }
        Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
           // 重点是run方法 
            @Override
            public void run(Timeout timeout) throws Exception {
               // 又是lua脚本  判断是否存在,存在就调用pexpire 
                RFuture<Boolean> future = commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                        "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                            "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                            "return 1; " +
                        "end; " +
                        "return 0;",
                          Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
                // 监听事件中又 调用了自己  scheduleExpirationRenewal
                future.addListener(new FutureListener<Boolean>() {
                    @Override
                    public void operationComplete(Future<Boolean> future) throws Exception {
                        expirationRenewalMap.remove(getEntryName());
                        if (!future.isSuccess()) {
                            log.error("Can't update lock " + getName() + " expiration", future.cause());
                            return;
                        }
                        if (future.getNow()) {
                            // reschedule itself
                            scheduleExpirationRenewal(threadId);
                        }
                    }
                });
            }
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
        if (expirationRenewalMap.putIfAbsent(getEntryName(), task) != null) {
            task.cancel();
        }
    }

redissonLock.unlock();逻辑

@Override
    public void unlock() {
        Boolean opStatus = get(unlockInnerAsync(Thread.currentThread().getId()));
        if (opStatus == null) {
            throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                    + id + " thread-id: " + Thread.currentThread().getId());
        }
        if (opStatus) {
            cancelExpirationRenewal();
        }
    }

重点看 unlockInnerAsync(Thread.currentThread().getId())

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "if (redis.call('exists', KEYS[1]) == 0) then " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; " +
                "end;" +
                "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                    "return nil;" +
                "end; " +
                "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                "if (counter > 0) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                    "return 0; " +
                "else " +
                    "redis.call('del', KEYS[1]); " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; "+
                "end; " +
                "return nil;",
                Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
    }

又是lua脚本,核心就是 把value减到为0 ,删除key

KEYS[1] ---------> getName()

KEYS[2] ---------> getChannelName()

ARGV[1] ---------> LockPubSub.unlockMessage

ARGV[2] ---------> internalLockLeaseTime

ARGV[2] ---------> getLockName(threadId)


总结

需要用到续锁功能时,一要记住不要设置锁的过期时间,可以设置成-1.

一旦设了时间,RedissonLock就会认为你需要自己控制锁时间,而放弃执行续锁逻辑。

查看源码, 续锁逻辑需要起定时器。所以要注意这点,并不是所有分布式场景都需要续锁逻辑的。当我们很难判断业务逻辑的执行时间时,不妨开启续锁。

至此,原理和源码我们粗略的梳理完了 ,梳理了主要的核心流程,主要是依靠lua脚本,代码写的还是非常优秀的,向开源学习!!!


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
12天前
|
存储 算法 Java
解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用
在Java中,Set接口以其独特的“无重复”特性脱颖而出。本文通过解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用。
30 3
|
17天前
|
存储 缓存 算法
分布式锁服务深度解析:以Apache Flink的Checkpointing机制为例
【10月更文挑战第7天】在分布式系统中,多个进程或节点可能需要同时访问和操作共享资源。为了确保数据的一致性和系统的稳定性,我们需要一种机制来协调这些进程或节点的访问,避免并发冲突和竞态条件。分布式锁服务正是为此而生的一种解决方案。它通过在网络环境中实现锁机制,确保同一时间只有一个进程或节点能够访问和操作共享资源。
40 3
|
5天前
|
数据采集 存储 编解码
一份简明的 Base64 原理解析
Base64 编码器的原理,其实很简单,花一点点时间学会它,你就又消除了一个知识盲点。
26 3
|
17天前
|
消息中间件 中间件 数据库
NServiceBus:打造企业级服务总线的利器——深度解析这一面向消息中间件如何革新分布式应用开发与提升系统可靠性
【10月更文挑战第9天】NServiceBus 是一个面向消息的中间件,专为构建分布式应用程序设计,特别适用于企业级服务总线(ESB)。它通过消息队列实现服务间的解耦,提高系统的可扩展性和容错性。在 .NET 生态中,NServiceBus 提供了强大的功能,支持多种传输方式如 RabbitMQ 和 Azure Service Bus。通过异步消息传递模式,各组件可以独立运作,即使某部分出现故障也不会影响整体系统。 示例代码展示了如何使用 NServiceBus 发送和接收消息,简化了系统的设计和维护。
32 3
|
21天前
|
开发框架 缓存 前端开发
electron-builder 解析:你了解其背后的构建原理吗?
本文首发于微信公众号“前端徐徐”,详细解析了 electron-builder 的工作原理。electron-builder 是一个专为整合前端项目与 Electron 应用的打包工具,负责管理依赖、生成配置文件及多平台构建。文章介绍了前端项目的构建流程、配置信息收集、依赖处理、asar 打包、附加资源准备、Electron 打包、代码签名、资源压缩、卸载程序生成、安装程序生成及最终安装包输出等环节。通过剖析 electron-builder 的原理,帮助开发者更好地理解和掌握跨端桌面应用的构建流程。
49 2
|
2天前
|
供应链 安全 分布式数据库
探索区块链技术:从原理到应用的全面解析
【10月更文挑战第22天】 本文旨在深入浅出地探讨区块链技术,一种近年来引起广泛关注的分布式账本技术。我们将从区块链的基本概念入手,逐步深入到其工作原理、关键技术特点以及在金融、供应链管理等多个领域的实际应用案例。通过这篇文章,读者不仅能够理解区块链技术的核心价值和潜力,还能获得关于如何评估和选择适合自己需求的区块链解决方案的实用建议。
8 0
|
14天前
|
前端开发 JavaScript UED
axios取消请求CancelToken的原理解析及用法示例
axios取消请求CancelToken的原理解析及用法示例
52 0
|
17天前
|
存储 缓存 数据处理
深度解析:Hologres分布式存储引擎设计原理及其优化策略
【10月更文挑战第9天】在大数据时代,数据的规模和复杂性不断增加,这对数据库系统提出了更高的要求。传统的单机数据库难以应对海量数据处理的需求,而分布式数据库通过水平扩展提供了更好的解决方案。阿里云推出的Hologres是一个实时交互式分析服务,它结合了OLAP(在线分析处理)与OLTP(在线事务处理)的优势,能够在大规模数据集上提供低延迟的数据查询能力。本文将深入探讨Hologres分布式存储引擎的设计原理,并介绍一些关键的优化策略。
62 0
|
22天前
|
SQL 分布式计算 大数据
大数据-97 Spark 集群 SparkSQL 原理详细解析 Broadcast Shuffle SQL解析过程(一)
大数据-97 Spark 集群 SparkSQL 原理详细解析 Broadcast Shuffle SQL解析过程(一)
33 0
|
22天前
|
SQL 分布式计算 算法
大数据-97 Spark 集群 SparkSQL 原理详细解析 Broadcast Shuffle SQL解析过程(二)
大数据-97 Spark 集群 SparkSQL 原理详细解析 Broadcast Shuffle SQL解析过程(二)
61 0

热门文章

最新文章

推荐镜像

更多