Redis全面解析三:redis分布式锁的实现原理你了解吗

前言

分布式概念提出之前,项目结构基本都是通过单机部署,针对多线程并发问题,java为我们已经提供了各种锁来解决问题。随着用户量的提升单体服务已经不能满足高并发场景的需求,于是兴起了分布式系统以及微服务的理念。由此引出分布式锁的概念,在多台机器与客户端之间引入一个分布式锁层,在高并发场景当多个线程访问服务器资源时,可以通过不同的机器对共享资源进行操作,jdk锁保证了单台机器内的线程安全(即单机里的多线程场景是共享堆内存的),但是多线程多个机器的访问的场景,jdk自带的锁已经不解决资源访问一致性问题。通过分布式锁层对多个线程进行控制,获取到锁的线程才能对集群中的资源进行访问操作,因此保证了线程安全。本文主要讲解redis分布式锁原理。

分布式锁

1、分布式锁背景

目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。 在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。 

2、分布式锁定义

在分布式环境下(即多台机器),某台计算机上的堆内存中的变量对于其他计算机上的线程肯定是不可见的。那么,根据锁的本质和原理,我们就要找到另外的对于多机上的线程都可见的标志,以它来作为锁,就可以了。这样的锁,就是分布式锁。 

3、分布式锁常见的三种实现方式:

  • 数据库乐观锁;
  • 基于Redis的分布式锁;
  • 基于ZooKeeper的分布式锁。

你对Redis使用熟悉吗?Redis中是如何实现分布式锁的。

Redis中分布式锁的实现机制

Redis要实现分布式锁,以下条件应该得到满足

  • 互斥性:在任意时刻,只有一个客户端能持有锁。
  • 不能死锁:客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  • 容错性:只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。

普通实现

说道Redis分布式锁大部分人都会想到:setnx+lua,或者知道set key value px milliseconds nx。Set 这个命令,目前已经支持这么多参数可选:

SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]
  • EX seconds : 将键的过期时间设置为 seconds 秒。 执行 SET key value EX seconds 的效果等同于执行 SETEX key seconds value 。
  • PX milliseconds : 将键的过期时间设置为 milliseconds 毫秒。 执行 SET key value PX milliseconds 的效果等同于执行 PSETEX key milliseconds value 。
  • NX : 只在键不存在时, 才对键进行设置操作。 执行 SET key value NX 的效果等同于执行 SETNX key value 。
  • XX : 只在键已经存在时, 才对键进行设置操作。

Note:因为 SET 命令可以通过参数来实现 SETNX 、 SETEX 以及 PSETEX 命令的效果, 所以 Redis 将来的版本可能会移除并废弃 SETNX 、 SETEX 和 PSETEX 这三个命令。

后一种方式的核心实现命令如下:

- 获取锁(unique_value可以是UUID等)
SET resource_name unique_value NX PX 30000

- 释放锁(lua脚本中,一定要比较value,防止误解锁)
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

 这种实现方式有3大要点(也是面试概率非常高的地方):

  1. set命令要用set key value px milliseconds nx:替代 setnx + expire 需要分两次执行命令的方式,保证了原子性。
  2. value要具有唯一性:可以使用UUID.randomUUID().toString()方法生成,用来标识这把锁是属于哪个请求加的,在解锁的时候就可以有依据;
  3. 释放锁时要验证value值,不能误解锁:同时利用了eval命令执行Lua脚本的原子性

存在的风险:事实上这类琐最大的缺点就是它加锁时只作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况,导致出现多个客户端持有锁的情况,这样就不能实现资源的独享了:

  1. 客户端A从master获取到锁
  2. 在master将锁同步到slave之前,master宕掉了(Redis的主从同步通常是异步的)。
    主从切换,slave节点被晋级为master节点
  3. 客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。导致存在同一时刻存不止一个线程获取到锁的情况。

官方推荐的集群方案:以上风险针对此集群方案,可查看前篇了解:https://blog.csdn.net/Mr_lisj/article/details/105890666

Redis 集群没有使用一致性hash, 而是引入了 哈希槽的概念.Redis 集群有16384个哈希槽(slot,每个key通过CRC16校验后对16384取模来决定放置哪个槽.集群的每个节点负责一部分hash槽.

redis-cluster

正因为如此,Redis作者antirez基于分布式环境下提出了一种更高级的分布式锁的实现方式:Redlock。笔者认为,Redlock也是Redis所有分布式锁实现方式中唯一能让面试官高潮的方式。

Redlock算法实现

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

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

ãå¼åèæé¿ãé¢è¯æ¶è¢«é®å°Rediséæä¹åï¼

 

  1. 如果你不熟悉 Redis 高可用部署,那么没关系。RedLock 算法虽然是需要多个实例,但是这些实例都是独自部署的,没有主从关系。
  2. RedLock 作者指出,之所以要用独立的,是避免了 Redis 异步复制造成的锁丢失,比如:主节点没来的及把刚刚 Set 进来这条数据给从节点,就挂了。
  3. 有些人是不是觉得大佬们都是杠精啊,天天就想着极端情况。其实高可用嘛,拼的就是 99.999...% 中小数点后面的位数。

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

  • 获取当前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实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。

总结:在锁失效时间、获取锁超时时间、锁使用时间 正常情况下,redlock算法认为,只要 N/2+1 个节点加锁成功,那么就认为获取了锁, 解锁时将所有实例解锁。

Redis 官方给出了以上两种基于 Redis 实现分布式锁的方法,详细说明可以查看:

https://redis.io/topics/distlock 。

Redisson

Redisson 是 Java 的 Redis 客户端之一,提供了一些 API 方便操作 Redis。Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

Redisson 帮我们搞了分布式的版本。比如 AtomicLong,直接用 RedissonAtomicLong 就行了,连类名都不用去新记,很人性化了。锁只是它的冰山一角,并且从它的 Wiki 页面看到,对主从,哨兵,集群等模式都支持,当然了,单节点模式肯定是支持的。

RedissonLock是可重入的,并且考虑了失败重试,可以设置锁的最大等待时间, 在实现上也做了一些优化,减少了无效的锁申请,提升了资源的利用率。

NOTE:需要特别注意的是,RedissonLock 同样没有解决 节点挂掉的时候,存在丢失锁的风险的问题。而现实情况是有一些场景无法容忍的,所以 Redisson 提供了实现了redlock算法的 RedissonRedLock,RedissonRedLock 真正解决了单点失败的问题,代价是需要额外的为 RedissonRedLock 搭建Redis环境。

所以,如果业务场景可以容忍这种小概率的错误,则推荐使用 RedissonLock, 如果无法容忍,则推荐使用 RedissonRedLock。

Redisson 和 Jedis区别

  • Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持;
  • Jedis客户端实例不是线程安全的,所以需要通过连接池来使用Jedis。
  • Redisson的API是线程安全的,所以可以操作单个Redisson连接来完成各种操作。
  • Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。
  • Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。

 

文章参考:

https://www.toutiao.com/a6808355292833645070/

https://www.toutiao.com/a6758222821052137992/

https://www.jianshu.com/p/7e47a4503b87

推荐:https://www.toutiao.com/a6774311978644013579/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章