专栏首页开发技术Redisson 分布式锁实现之源码篇 → 为什么推荐用 Redisson 客户端

Redisson 分布式锁实现之源码篇 → 为什么推荐用 Redisson 客户端

开心一刻

  一男人站在楼顶准备跳楼,楼下有个劝解员拿个喇叭准备劝解

  劝解员:兄弟,别跳

  跳楼人:我不想活了

  劝解员:你想想你媳妇

  跳楼人:媳妇跟人跑了

  劝解员:你还有兄弟

  跳楼人:就是跟我兄弟跑的

  劝解员:你想想你家孩子

  跳楼人:孩子是他俩的

  劝解员:死吧,妈的你活着也没啥价值了

前言

  关于锁,相信大家都不陌生,一般我们用其在多线程环境中控制对共享资源的并发访问

  单服务下,用 JDK 中的 synchronized 或 Lock 的实现类可实现对共享资源的并发访问

  分布式服务下,JDK 中的锁就显得力不从心了,分布式锁也就应运而生了

  分布式锁的实现方式有很多,常见的有如下几种

    基于 MySQL,利用行级悲观锁(select ... for update)

    基于 Redis,利用其 (setnx + expire) 或 set

    基于 Zookeeper,利用其临时目录和事件回调机制

  具体的实现细节就不展开了,网上资料很多

  看下文之前最好先看下:Redisson 分布式锁实现之前置篇 → Redis 的发布/订阅 与 Lua,方便更好的理解下文

分布式锁的特点

  可以类比 JDK 中的锁

  互斥

    不仅要保证同个服务中不同线程的互斥,还需要保证不同服务间、不同线程的互斥

    如何处理互斥,是自旋、还是阻塞 ,还是其他 ?

  超时

    锁超时设置,防止程序异常奔溃而导致锁一直存在,后续同把锁一直加不上

  续期

    程序具体执行的时长无法确定,所以过期时间只能是个估值,那么就不能保证程序在过期时间内百分百能运行完

    所以需要进行锁续期,保证业务能够正常执行完

  可重入

    可重入锁又名递归锁,是指同一个线程在外层方法已经获得锁,再进入该线程的中层或内层方法会自动获取锁

    简单点来说,就是同个线程可以反复获取同一把锁

  专一释放

    通俗点来讲:谁加的锁就只有它能释放这把锁

    为什么会出现这种错乱释放的问题了,举个例子就理解了

      线程 T1 对资源 lock_zhangsan 加了锁,由于某些原因,加锁业务还未执行完,锁过期自动释放了,此时线程 T2 对资源 lock_zhangsan 加锁成功

      T2 执行业务的时候,T1 业务执行完后释放资源 lock_zhangsan 的锁,结果把 T2 加的锁给释放了

  公平与非公平

    公平锁:多个线程按照申请锁的顺序去获得锁,所有线程都在队列里排队,这样就保证了队列中的第一个先得到锁

    非公平锁:多个线程不按照申请锁的顺序去获得锁,而是同时直接去尝试获取锁

    JDK 中的 ReentrantLock 就有公平和非公平两种实现,有兴趣的可以去看看它的源码

    多数情况下用的是非公平锁,但有些特殊情况下需要用公平锁

  很多小伙伴觉得:引入一个简单的分布式锁,有必要考虑这么多吗?

  虽然绝大部分情况下,我们的程序都是在跑正常流程,但不能保证异常情况 100% 跑不到,出于健壮性考虑,异常情况都需要考虑到

  下面我们就来看看 Redisson 是如何实现这些特点的

Redisson 实现分布式锁

  关于 Redisson,更多详细信息可查看官方文档

  Redisson 是 Redis 官方推荐的 Java 版的 Redis 客户端,它提供了非常丰富的功能,其中就包括本文关注的分布式锁

  环境准备

    简单示例开始之前,我们先看下环境;版本不同,会有一些差别

    JDK:1.8

    Redis:3.2.8

    Redisson:3.13.6

  简单示例

    先将 Redis 信息配置给 Redisson,创建出 RedissonClient

    Redis 的部署方式不同,Redisson 配置模式也会不同,详细信息可查看:Configuration

    我们就配置最简单的 Single instance mode

    RedissonClient 创建出来后,就可以通过它来获取锁

    完整示例代码:redisson-demo

  接下来我们从源码层面一起看看 Redisson 具体是如何实现分布式锁的特点的

客户端创建

  客服端的创建过程中,会生成一个 id 作为唯一标识,用以区分分布式下不同节点中的客户端

  id 值就是一个 UUID,客户端启动时生成

  那么这个 id 有什么用,大家暂且在脑中留下这个疑问,我们接着往下看

锁的获取

  我们从 lock 开始跟源码

  最终会来到有三个参数的 lock 方法

    private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        
        // 尝试获取锁;ttl为null表示锁获取成功; ttl不为null表示获取锁失败,其值为其他线程占用该锁的剩余时间
        Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {
            return;
        }

        // 锁被其他线程占用而获取失败,使用redis的发布订阅功能来等待锁的释放通知,而非自旋监测锁的释放
        RFuture<RedissonLockEntry> future = subscribe(threadId);
        
        // 当前线程会阻塞,直到锁被释放时当前线程被唤醒(有超时等待,默认 7.5s,而不会一直等待)
        // 持有锁的线程释放锁之后,redis会发布消息,所有等待该锁的线程都会被唤醒,包括当前线程
        if (interruptibly) {
            commandExecutor.syncSubscriptionInterrupted(future);
        } else {
            commandExecutor.syncSubscription(future);
        }

        try {
            while (true) {
                // 尝试获取锁;ttl为null表示锁获取成功; ttl不为null表示获取锁失败,其值为其他线程占用该锁的剩余时间
                ttl = tryAcquire(-1, leaseTime, unit, threadId);
                // lock acquired
                if (ttl == null) {
                    break;
                }

                // waiting for message
                if (ttl >= 0) {
                    try {
                        // future.getNow().getLatch() 返回的是 Semaphore 对象,其初始许可证为 0,以此来控制线程获取锁的顺序
                        // 通过 Semaphore 控制当前服务节点竞争锁的线程数量
                        future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    } catch (InterruptedException e) {
                        if (interruptibly) {
                            throw e;
                        }
                        future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    }
                } else {
                    if (interruptibly) {
                        future.getNow().getLatch().acquire();
                    } else {
                        future.getNow().getLatch().acquireUninterruptibly();
                    }
                }
            }
        } finally {
            // 退出锁竞争(锁获取成功或者放弃获取锁),则取消锁的释放订阅
            unsubscribe(future, threadId);
        }
//        get(lockAsync(leaseTime, unit));
    }

  主要是三个点:尝试获取锁、订阅、取消订阅;我们一个一个来看

  尝试获取锁

  尝试获取锁主要做了两件事:1、尝试获取锁,2、锁续期

  尝试获取锁主要涉及到一段 lua 代码

  结合我的上篇文章来看,这个 lua 脚本还是很好理解的

    1、用 exists 判断 key 不存在,则用 hash 结构来存放锁,key = 资源名,field = uuid + : + threadId,value 自增 1

      设置锁的过期时间(默认是 lockWatchdogTimeout = 30 * 1000 毫秒),并返回 nil

    2、用 hexists 判断 field = uuid + : + threadId 存在

      则该 field 的 value 自增 1,并重置过期时间,最后返回 nil

这里相当于实现了锁的重入

    3、上面两种情况都不满足,则说明锁被其他线程占用了,直接返回锁的过期时间

  这里有个疑问:为什么 field = uuid + : + threadId,而不是 field = threadId

    友情提示下:从多个服务(也就是多个 Redisson 客户端)来考虑

    这个问题想清楚了,那么前面提到的:在 Redisson 客户端创建的过程中生成的 id(一个随机的 uuid 值),它的作用也就清楚了

  在获取锁成功之后,会启一个定时任务实现锁续期,也涉及到一段 lua 脚本

  这段脚本很简单,相信大家都能看懂

  默认情况下,锁的过期时间是 30s,锁获取成功之后每隔 10s 进行一次锁续期,重置过期时间成 30s

  若锁已经被释放了,则定时任务也会停止,不会再续期

  订阅

  获取锁的过程中,尝试获取锁失败(锁被其他线程锁占有),则会完成对该锁频道的订阅,订阅过程中线程会阻塞

  持有锁的线程释放锁时会向锁频道发布消息,订阅了该锁频道的线程会被唤醒,继续去获取锁

  这里有个疑问:假设持有锁的线程意外停止了,未向锁频道发布消息,那订阅了锁频道的线程该如何唤醒

    Redisson 其实已经考虑到了

    有超时机制,默认超时时长 = 3000 + 1500 * 3 = 7500 毫秒

  再提个问题:为什么要用 Redis 的发布订阅

    假设我们不用 Redis 的发布订阅,我们该如何实现,自旋?

    自旋有什么缺点? 自旋频率难以掌控,太高会增大 CPU 的负担,太低会不及时(锁都释放半天了才检测到)

    可以类比 生产者与消费者 来考虑这个问题

  取消订阅

  有订阅,肯定就有取消订阅;当阻塞的线程被唤醒并获取到锁时需要取消对锁频道的订阅

  当然,取消获取锁的线程也需要取消对锁频道的订阅

  比较好理解,就是取消当前线程对锁频道的订阅

锁的释放

  我们从 unlock 开始

  代码比较简单,我们继续往下跟

  主要有两点:1、锁释放,2、取消续期定时任务

  锁释放

    重点在于一个 lua 脚本

    我们把参数具象化,脚本就好理解了

      KEYS[1] = 锁资源,KEYS[2] = 锁频道

      ARGV[1] = 锁频道消息类型,ARGV[2] = 过期时间,ARGV[3] = uuid + : + threadId

    1、如果当前线程未持有锁,直接返回 nil

    2、hash 结构的 field 的 value 自减 1,counter = 自减后的 value 值

      如果 counter > 0,表示线程重入了,重置锁的过期时间,返回 0

      如果 counter <= 0,删除锁,并对锁频道发布锁释放消息(频道订阅者则可收到消息,然后唤醒线程去获取锁),返回 1

    3、上面 1、2 都不满足,则直接返回 nil

    两个细节:1、重入锁的释放,2、锁彻底释放后的消息发布

  取消续期定时任务

  比较简单,没什么好说的

总结

  我们从分布式锁的特点出发,来总结下 Redisson 是如何实现这些特点的

  互斥

  Redisson 采用 hash 结构来存锁资源,通过 lua 脚本对锁资源进行操作,保证线程之间的互斥

  互斥之后,未获取到锁的线程会订阅锁频道,然后进入一定时长的阻塞

  超时

  有超时设置,给 hash 结构的 key 加上过期时间,默认是 30s

  续期

  线程获取到锁之后会开启一个定时任务(watchdog),每隔一定时间(默认 10s)重置 key 的过期时间

  可重入

  通过 hash 结构解决,key 是锁资源,field 是持有锁的线程,value 表示重入次数

  专一释放

  通过 hash 结构解决,field 中存放了线程信息,释放的时候就能够知道是不是线程加上的锁,是才能够进行锁释放

  公平与非公平

  留给大家补充

参考

再有人问你分布式锁,这篇文章扔给他

拜托,面试请不要再问我Redis分布式锁的实现原理!【石杉的架构笔记】

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Redisson 分布式锁源码 09:RedLock 红锁的故事

    所以本文会先介绍什么是 RedLock,当大家对 RedLock 有一个基本的了解。然后再看 Redisson 中是如何实现 RedLock 的。

    程序员小航
  • Redis集群实现分布式锁的正确方式

    上文我们介绍的 Redis实现分布式锁的正确方式 是 redis 单机的方式,所以本篇要基于 redis 集群做分布式锁,我们使用 Redisson

    胖虎
  • 如何用Redlock实现分布式锁

    之前写过一篇文章《如何在springcloud分布式系统中实现分布式锁?》,由于自己仅仅是阅读了相关的书籍,和查阅了相关的资料,就认为那样的是可行的。那篇文章实...

    方志朋
  • 聊一聊Redis官方置顶推荐的Java客户端Redisson

    写这篇的时候,相信有很多朋友还在用Jedis作为Redis的客户端,我不禁有很多问号,Jedis还香吗?如果你早些年说它香我信,但是都2020年了,它真的不那么...

    JavaQ
  • 手撕redis分布式锁,隔壁张小帅都看懂了!

    看到标题,有人可能会满心疑惑?张小帅是谁?咳咳,老猫在此先卖个关子,关于张小帅,老猫后续会向大家正式介绍的,标题算是彩蛋了。

    程序员老猫
  • 【大厂面试题】Redis中是如何实现分布式锁的?

    可以直接通过 set key value px milliseconds nx 命令实现加锁, 通过Lua脚本实现解锁。

    民工哥
  • 分布式锁中的王者方案-Redisson

    上篇讲解了如何用 Redis 实现分布式锁的五种方案,但我们还是有更优的王者方案,就是用 Redisson。

    悟空聊架构
  • 又长又细,万字长文带你解读Redisson分布式锁的源码

    上一篇文章写了Redis分布式锁的原理和缺陷,觉得有些不过瘾,只是简单的介绍了下Redisson这个框架,具体的原理什么的还没说过呢。趁过年放几天假,反正闲着也...

    鄙人薛某
  • 如何优雅的实现分布式锁?(文末赠书)

    随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发...

    范蠡
  • 分布式锁用Redis还是Zookeeper?

    系统A是一个电商系统,目前是一台机器部署,系统中有一个用户下订单的接口,但是用户下订单之前一定要去检查一下库存,确保库存足够了才会给用户下单。

    悟空聊架构
  • 万字长文!分布式锁的实现全都在这里了

    最近老板接了一个大单子,允许在某终端设备安装我们的APP,终端设备厂商日活起码得几十万到百万级别,这个APP也是近期产品根据市场竞品分析设计出来的,几个小码农通...

    数据和云
  • 面试被问Redis锁的缺点,被打击的扎心了

    来源:juejin.im/post/5e61a454e51d4526f071e1df

    业余草
  • 分布式锁中的王者方案 - Redisson

    如果你之前是在用 Redis 的话,那使用 Redisson 的话将会事半功倍,Redisson 提供了使用 Redis的最简单和最便捷的方法。

    macrozheng
  • 蚂蚁金服面试:如何优雅的用Redis实现分布式锁?

    上述代码可以看到,当前锁的失效时间为10s,如果当前扣减库存的业务逻辑执行需要15s时,高并发时会出现问题:

    Java程序猿
  • 死磕 java同步系列之redis分布式锁进化史

    Redis(全称:Remote Dictionary Server 远程字典服务)是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、...

    彤哥
  • Redis,Zookeeper究竟哪种可以优化实现分布式锁

    原文:https://www.enmotech.com/web/detail/1/775/1.html

    数据和云01
  • 分布式锁用Redis坚决不用Zookeeper?

    系统 A 是一个电商系统,目前是一台机器部署,系统中有一个用户下订单的接口,但是用户下订单之前一定要去检查一下库存,确保库存足够了才会给用户下单。

    数据和云
  • Redis 和 Zookeeper 到底谁更牛?

    系统A是一个电商系统,目前是一台机器部署,系统中有一个用户下订单的接口,但是用户下订单之前一定要去检查一下库存,确保库存足够了才会给用户下单。

    开发者技术前线
  • 不懂分布式锁的这些问题,就亏大了

    墨墨导读:通过一个很常见的业务场景,引出一个分布式锁的具体方案,如何使用分布式锁呢?通过本文了解下。

    数据和云

扫码关注云+社区

领取腾讯云代金券