Redis分布式锁实现原理

为什么需要分布式锁

在早期互联网的架构中,一个应用都是单机进行部署,这种情况下,利用JDK提供的锁机制即可解决共享数据在多线程场景下的线程安全问题,但随着技术的发展,分布式系统架构逐渐普及,在分布式架构中,由于一个应用会进行多机部署,服务器实例之间的JVM是互相独立的, 这时候利用JDK提供的锁在这种场景下是没办法共享的 ,所以需要依靠一个中间件实现在分布式的场景下对锁的共享,典型的如 **Redis **。

为什么是Redis

  • 由于Redis中的数据是存放在内存中,读写速度很快,没有磁盘的IO,所以加锁和释放锁的速度会很快,性能很高。
  • Redis对客户端的请求都是单进程单线程进行处理的,也就意味着串行化去执行的,所以Redis的单个命令是支持原子性的,即使对于多个Redis指令,Redis也提供了将多个指令合并在一个Lua脚本中一起执行,从而保证多条指令的原子性操作。

分布式锁的加锁和解锁的要求必须是原子性的,而Redis就可以很好的支持这一特性。

如何利用Redis实现锁机制

用一句话概括的说,其实Redis实现锁机制其实就是在Redis中设置一个key-value,当key存在时,即上锁,删除key即解锁。
当然要想实现一个很健壮的锁机制,这其中还有很多细节不容忽视,所以下面,我们一步一步的跟着思路去思考如何使用Redis实现一个分布式的锁:

  1. 加锁保证互斥性,同一时间只能有一个客户端加锁成功。

    • 通过Redis的setnx命令实现,setnx即 set if not exists,当key不存在时才能设置成功

      SETNX key value
           summary: Set the value of a key, only if the key does not exist
           since: 1.0.0
           group: string
      
    • (推荐)通过set key value PX 3000 NX,PX指过期时间,NX即not exists,效果等同setnx,但是由于 setnx 不支持设置过期时间,所以需要拆分成两个两个命令setnx key valueexpire key 3,要保证原子性还需要将两个命令合并为一个lua脚本。

      SET key value [EX seconds|PX milliseconds|EXAT timestamp|PXAT milliseconds-timestamp|KEEPTTL] [NX|XX] [GET]
        summary: Set the string value of a key
        since: 1.0.0
        group: string
      

    前面也提到,由于Redis是单线程的,所以当大量请求过来时,这些请求是串行化执行的,所以一定只有一个请求才能设置成功,从而保证了加锁的互斥性。

  2. 防止死锁
    当客户端加锁之后,在释放锁之前如果Redis发生了宕机,那么Redis中的锁就无法自动释放,最终产生死锁,所以为了避免死锁,我们还需要给这个锁的key设置一个合理的过期时间,当锁占用的时间超过指定的过期时间,则自动删除该锁对应的key释放锁,让其他客户端能够有机会去争抢这个锁。

  3. 锁过期提前释放
    上一步由于为了避免死锁,所以在加锁时,指定了锁的有效期,但是这个有效期也是估算出来的,如果实际业务处理时间超过了锁的有效期,锁会被提前释放,就会导致其他客户端获得了锁,从而导致锁机制的失效。
    所以为了解决该问题,就需要一个机制去对锁进行续期,防止在加锁的业务还未处理完之前,被提前释放,我们可以利用一个子线程,在锁有效期到期之前,定期的去的给锁进行续期,即:增加key的过期时间。

  4. 释放锁
    释放锁,只需要将对应的锁的key从redis中删除即可,但是这里需要注意的是,在释放锁之前,必须判断只有是当前线程占用的锁才可以进行释放,所以锁的key对应的value我们就可以存放当前的客户端的身份标识,在释放锁之前,比对一下当前释放锁的客户端是否是当前加锁的客户端,如果匹配成功则可以正常删除对应的key释放锁,否则就不释放锁。

综合以上的对Redis实现锁的思路分析,其实市面上已经有了成熟的开源框架的实现,就是今天的主角 RedissonRedisson 不仅实现了基于Redis实现加锁,解锁,还提供了防死锁,锁续期,以及可重入的锁的功能,可以说能够满足大多数的场景了,下面我们就看下Redisson底层是如何实现Redis分布式锁的。

Redisson原理

Redisson提供RLock的接口,继承了JUC包下的java.util.concurrent.locks.Lock, 所以加锁的方式和JDK提供的ReentrantLock加锁方式很类似。
我们先来看一段利用Redisson加锁的代码:

        // 获取锁对象
        RLock lock = redissonClient.getLock("myLock");
        try {
            // 加锁, 5 代表锁过期自动释放的时间,单位为 秒
            boolean locked = lock.tryLock(5, TimeUnit.SECONDS);

            if (locked) {
                // 处理业务逻辑
            } else {
                // 未获取锁的逻辑
            }
        } finally {
            // 只有持有锁的线程才能释放锁
            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
  1. 获取锁对象 RLock lock = redissonClient.getLock("myLock");

    这里myLock就是指定Redis中锁的唯一标识,关于key值需要根据实际业务来确定,一定要保证唯一性,而且key值也决定了锁粒度的大小。

  2. 加锁

    Redisson常用的加锁方式如:lock,tryLock,方式的区别如下:

    • lock方式,无返回值,如果锁已经被其他客户端持有,会利用Redis中的PUB/SUB机制,订阅Redisson解锁消息,并且当前线程会进行await阻塞,当监听到解锁消息,阻塞的客户端会被唤醒进行锁争夺。

      在这里插入图片描述

    • tryLock 方式是有返回值的,当加锁失败会返回一个false,交由调用方决定后续的操作。

    对于以上两种加锁方式,最终都会调用下面这段Lua脚本的代码进行加锁操作:

在这里插入图片描述

对于上述的Lua脚本解读如下:

a. 第一步通过 exists key 去判断锁的key是否存在.

b. 第二步,如果第一步中的判断返回0,表示 key 不存在,这时候可以加锁,利用hset key value 设置keyvalue,即ARGV[2].

  > hset 代表这个key对应的value是一个hash类型,类似于HashMap,其中field代表客户端,value是重入次数。
  >
  > ARGV[2] 代表了加锁客户端的唯一标识,由UUID和线程id组成,可以理解为某个客户端的某个线程加锁。

c. 第三步设置key的存活时间internalLockLeaseTime,这里 ARGV[1] 代表的是锁 key 的默认生存时间,默认 30 秒。

如果a步骤中的exists key判断key已经存在,即exists KEYS[1] 返回1,则利用hexists key field 判断当前的客户端ID(即ARG[2])在锁的key对应的hash数据结构中是否存在

  • 存在表明是当前客户端持有的锁,这时候就相当于锁重入,就利用hincrby key field increment去对锁重入进行 + 1,并通过pexpire key millseconds设置过期时间。
  • 不存在则表明当前持有锁是另外一个客户端,所以直接退出第二个if判断

如果上述两个if条件都未满足,则pttl key 返回当前锁的key的剩余存活时间。

  1. 从上述描述可以看出,锁的key其实可以设置过期时间的,key一旦过期,redis就会清除这个key,如果当业务处理的时间超出了锁的有效期,这时候锁就会被其他客户端获取成功,会造成锁失效,所以在Redisson中还存在一个WatchDog的机制去对去定期(默认10秒)去给锁续期,即Redisson会开启定时任务TimerTask去定时对锁的有效期进行延长。
    这里需要注意的是,WatchDog机制只有在我们未手工指定对应的锁过期时间leaseTime才会生效

    在这里插入图片描述

  2. unlock 释放锁,释放锁的时候需要判断当前的客户端(UUID + threadId)是否持有锁,只有持有锁的客户端才能释放锁。

    在这里插入图片描述

    unlock 其实最终也是通过Lua脚本进行解锁:

    a. 通过hexists lockKey clientId判断是否当前客户端持有锁,如果不是,则直接返回

    b. 如果是当前客户端持有锁,则对锁的重入次数counter进行-1,如果-1之后重入次数依旧大于0,说明锁被重入,需等待重入次数为0才可以解锁,这时候重新设置锁key的有效期。

    c. 如果重入次数已经为0,则开始释放锁,即del lockKey,并且发布一个锁释放的消息到channel中,通知其他等待锁的客户端进行加锁操作。


以上就是Redisson分布式锁的实现原理。

Redisson 单机模式下的缺点

事实上这类锁最大的缺点就是它加锁时只作用在一个Redis节点上,如果Redis挂了,那么就会产生单点故障的问题,
即使Redis通过sentinel哨兵机制保证高可用,当master节点发生故障后,可以故障转移,slaver升级为master,
但由于主从之间的数据同步是异步的, 如果在发生主从切换的时候,key 还没来得及同步到slaver上,那么就会出现锁丢失的情况:

  1. 在Redis的master节点上拿到了锁;
  2. 但是这个加锁的key还没有同步到slave节点;
  3. master故障,发生故障转移,slave节点升级为master节点;
  4. 导致锁丢失

所以Redis对于这种场景提供RedLock红锁,即对主节点master的Redis进行集群,多个master实例间互相独立,需要对N个实例进行上锁,这里假设有5个Redis集群,当获取锁的时候,当且仅当大多数的节点(即 N/2 + 1)都设置锁成功,整个获取锁的过程才算成功,如果没有满足该条件,就需要在向所有的Redis实例发送释放锁命令即可,不用关心之前有没有从Redis实例成功获取到锁.

这里顺手推荐一篇比较好的文章 https://segmentfault.com/a/1190000041172633

  • 7
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1、什么是 Redis? 2、Redis 相比 memcached 有哪些优势? 3、Redis 支持哪几种数据类型? 4、Redis 主要消耗什么物理资源? 5、Redis 的全称是什么? 6、Redis 有哪几种数据淘汰策略? 7、Redis 官方为什么不提供 Windows 版本? 8、一个字符串类型的值能存储最大容量是多少? 9、为什么 Redis 需要把所有数据放到内存中? 10、Redis 集群方案应该怎么做?都有哪些方案? 11、Redis 集群方案什么情况下会导致整个集群不可用? 12、MySQL 里有 2000w 数据,Redis 中只存 20w 的数据, 如何保证 Redis 中的数据都是热点数据? 13、Redis 有哪些适合的场景? 14、Redis 支持的 Java 客户端都有哪些?官方推荐用哪个? 15、RedisRedisson 有什么关系? 16、Jedis 与 Redisson 对比有什么优缺点? 17、Redis 如何设置密码及验证密码? 18、说说 Redis 哈希槽的概念? 19、Redis 集群的主从复制模型是怎样的? 20、Redis 集群会有写操作丢失吗?为什么? 21、Redis 集群之间是如何复制的? 22、Redis 集群最大节点个数是多少? 23、Redis 集群如何选择数据库? 24、怎么测试 Redis 的连通性? 25、Redis 中的管道有什么用? 26、怎么理解 Redis 事务? 27、Redis 事务相关的命令有哪几个? 28、Redis key 的过期时间和永久有效分别怎么设置? 29、Redis 如何做内存优化? 30、Redis 回收进程如何工作的? 31、Redis 回收使用的是什么算法? 32、Redis 如何做大量数据插入? 33、为什么要做 Redis 分区? 34、你知道有哪些 Redis 分区实现方案? 35、Redis 分区有什么缺点? 36、Redis 持久化数据和缓存怎么做扩容? 37、分布式 Redis 是前期做还是后期规模上来了再做好?为 什么? 38、Twemproxy 是什么? 39、支持一致性哈希的客户端有哪些? 40、Redis 与其他 key-value 存储有什么不同? 41、Redis 的内存占用情况怎么样? 42、都有哪些办法可以降低 Redis 的内存使用情况呢? 43、查看 Redis 使用情况及状态信息用什么命令? 44、Redis 的内存用完了会发生什么? 45、Redis 是单线程的,如何提高多核 CPU 的利用率? 46、一个 Redis 实例最多能存放多少的 keys?List、Set、 Sorted Set 他们最多能存放多少元素? 47、Redis 常见性能问题和解决方案? 48、Redis 提供了哪几种持久化方式? 49、如何选择合适的持久化方式? 50、修改配置不重启 Redis 会实时生效吗?

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值