redis的主从模式、哨兵模式、集群模式

0. 前言

之前写的是redis单机模式下的运行,只要是单机模式就不可避免的会出以下问题,单点故障,容量有限,压力等。
先说单点故障,解决单点故障,就需要有备份,主备模式可以解决单点问题,但是备机在主机正常的状态下,备机提供不了服务,有点浪费服务器资源,主从模式下,作为slave服务器不仅可以在主机故障时提供服务,平常也可以读写分离,缓解主机的压力,Master服务器可以读写,slave服务器提供可读服务。
再说下容量的问题,和关系型数据一样,数据量大的时候就要拆分,按照业务,功能拆分(有点类似于垂直拆分)。
连接压力,可能就要再拆分,根据逻辑(范围,hash)再拆分)

1. 主从模式

主从复制的主要作用

  • 数据冗余(可以相当于持久化的另一种形式)
  • 故障恢复(哨兵模式下必须要有从节点)
  • 读写分离(分担主节点压力)
  • 高可用的基础

redis官方文档-复制(官方文档说的特别详细,强烈建议详细观看)

1.1. 复制流程

Redis使用默认的异步复制,其特点是低延迟和高性能

  • 当一个 master 实例和一个 slave 实例连接正常时, master 会发送一连串的命令流来保持对 slave 的更新,以便于将自身数据集的改变复制给 slave :包括客户端的写入、key 的过期或被逐出等等。
  • 当 master 和 slave 之间的连接断开之后,因为网络问题、或者是主从意识到连接超时, slave 重新连接上 master 并会尝试进行部分重同步:这意味着它会尝试只获取在断开连接期间内丢失的命令流。
  • 当无法进行部分重同步时, slave 会请求进行全量重同步。这会涉及到一个更复杂的过程,例如 master 需要创建所有数据的快照,将之发送给 slave ,之后在数据集更改时持续发送命令流到 slave 。

1.1.1. slave第一次追随master时的过程

slave服务器:建立连接->接收数据(全量文件)->删除本地数据->加载从master传输的数据
在这里插入图片描述master:接收连接->持久化文件(bgsave)->传输数据给slave
在这里插入图片描述上述是第一次创建连接时,我们可以发现,master也可以发现有多少slave连接跟随他。

1.1.2. 设置slave

可以用命令指定跟随:replicaof 127.0.0.1 6379
salve除了上边的命令可以跟随master,还可以更改配置文件,通过指定IP和端口来定位到主redis的位置

slaveof <masterip> <masterport>  

如果slave想取消跟随 replicaof no one
设置slave只读

slave-read-only yes           (设置只读)

1.1.3. slave响应策略

从redis失去与主redis的连接,或者主从同步正在进行时,redis对外部发来的访问请求的两种处理选择:
yes(默认),从redis仍会继续响应客户端的读写请求
no,从redis会对客户端的请求返回“SYNC with master in progress”(例外:当客户端发来INFO请求和SLAVEOF请求,从redis还是会进行处理)

slave-serve-stale-data yes (设置继续响应旧数据)

1.2. Redis 复制功能是如何工作的

复制:每一个 Redis master 都有一个Replication ID(数据量id), offset(偏移量)

1.2.1. 全量复制

全量复制:master 开启一个后台保存进程,以便于生产一个 RDB 文件。同时它开始缓冲所有从客户端接收到的新的写入命令。当后台保存完成时, master 将数据集文件传输给 slave, slave将之保存在磁盘上,然后加载文件到内存。再然后 master 会发送所有缓冲的命令发给 slave。这个过程以指令流的形式完成并且和 Redis 协议本身的格式相同。

全量复制时,会有dump文件,需要将一个新的RDB文件dump出来,然后从主redis传到从redis。有两种可选:

  • 基于硬盘(disk-backed)(默认):主redis创建一个新进程dump RDB,之后由父进程(即主进程)增量传给从redis
  • 基于socket(diskless):主redis创建一个新进程直接dump RDB到从redis的socket,不经过主进程,不经过硬盘,Redis 2.8.18 是第一个支持无磁盘复制的版本

如基于硬盘,RDB文件创建后,一旦创建完毕,可以同时服务更多从redis;如基于socket,新的从redis加入后,需进行排队(如超出repl-diskless-sync-delay设定时时);当用diskless时,主redis等待repl-diskless-sync-delay设定的秒数后,如无新的从redis加入,就直接传,后来的需排队等待。

repl-diskless-sync no
repl-diskless-sync-delay 5             ##每五秒传输一次(需开启diskless)
repl-ping-slave-period 10             ##设置从redis向主redis发出PING包的周期(默认10秒)

1.2.2. 增量复制

增量复制:当 slave 连接到 master 时,它们使用 PSYNC/SYNC 命令来发送它们记录的旧的 master replication ID 和它们至今为止处理的偏移量。通过这种方式, master 能够仅发送 slave 所需的增量部分。但是如果 master 的缓冲区中没有足够的命令积压缓冲记录,或者如果 slave 引用了不再知道的历史记录(replication ID),则会转而进行一个全量重同步:在这种情况下, slave 会得到一个完整的数据集副本,从头开始。

队列长度(backlog)是主redis中的一个缓冲区,在与从redis断开连接期间,主redis会用这个缓冲区来缓存应该发给从redis的数据。这样,当从redis重新连接上后,就不必重新全量同步数据,只需要同步这部分增量数据即可

# repl-backlog-size 1mb

1.3.当 master 关闭持久化时,复制的安全性

在使用 Redis 复制功能时的设置中,强烈建议在 master 和在 slave 中启用持久化,如果关闭持久化,则应保证实例不会重置后自动重启。
master重启会清空数据,重启时间过短的话会让另外的salve复制,销毁副本数据,如果在sentinel(哨兵模式下),重启时间过短,会让哨兵没有探测到故障。

1.4.重新启动和故障转移后的部分重同步

当一个实例在故障转移后被提升为 master 时,它仍然能够与旧 master 的 slaves 进行部分重同步。为此,slave 会记住旧 master 的旧 replication ID 和复制偏移量,因此即使询问旧的 replication ID,其也可以将部分复制缓冲提供给连接的 slave 。
slave 在关机并重新启动后,能够在 RDB 文件中存储所需信息,以便与 master 进行重同步

1.5. Redis 复制如何处理 key 的过期

Redis 的过期机制可以限制 key 的生存时间。此功能取决于 Redis 实例计算时间的能力,但是,即使使用 Lua 脚本更改了这些 key,Redis slaves 也能正确地复制具有过期时间的 key。

为了实现这样的功能,Redis 不能依靠主从使用同步时钟,因为这是一个无法解决的并且会导致 race condition 和数据集不一致的问题,所以 Redis 使用三种主要的技术使过期的 key 的复制能够正确工作:

  1. slave 不会让 key 过期,而是等待 master 让 key 过期。当一个 master 让一个 key 到期(或由于 LRU 算法将之驱逐)时,它会合成一个 DEL 命令并传输到所有的 slave。
  2. 但是,由于这是 master 驱动的 key 过期行为,master 无法及时提供 DEL 命令,所以有时候 slave 的内存中仍然可能存在在逻辑上已经过期的 key 。为了处理这个问题,slave 使用它的逻辑时钟以报告只有在不违反数据集的一致性的读取操作(从主机的新命令到达)中才存在 key。用这种方法,slave 避免报告逻辑过期的 key 仍然存在。在实际应用中,使用 slave 程序进行缩放的 HTML 碎片缓存,将避免返回已经比期望的时间更早的数据项。
  3. 在Lua脚本执行期间,不执行任何 key 过期操作。当一个Lua脚本运行时,从概念上讲,master 中的时间是被冻结的,这样脚本运行的时候,一个给定的键要么存在要么不存在。这可以防止 key 在脚本中间过期,保证将相同的脚本发送到 slave ,从而在二者的数据集中产生相同的

2.哨兵模式

Sentinel
先说明一下,哨兵一定是建立在主从模式的基础上,只有有从节点,才可以自动故障转移
redis官方文档对-高可用

2.1. 任务

Redis 的 Sentinel 系统用于管理多个 Redis 服务器(instance), 该系统执行以下三个任务:

  • 监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
  • 提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。,
  • 自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。

2.2. 启动 Sentinel

对于 redis-sentinel 程序, 你可以用以下命令来启动 Sentinel 系统:
redis-sentinel
对于 redis-server 程序, 你可以用以下命令来启动一个运行在 Sentinel 模式下的 Redis 服务器:
redis-server /path/to/sentinel.conf --sentinel

两种方法都可以启动一个 Sentinel 实例。

启动 Sentinel 实例必须指定相应的配置文件, 系统会使用配置文件来保存 Sentinel 的当前状态, 并在 Sentinel 重启时通过载入配置文件来进行状态还原。

如果启动 Sentinel 时没有指定相应的配置文件, 或者指定的配置文件不可写(not writable), 那么 Sentinel 会拒绝启动。

sentinel monitor mymaster 127.0.0.1 6379 2 (监视一个名为 mymaster 的主服务器,主服务器的 IP 地址为 127.0.0.1 , 端口号为 6379,将这个主服务器判断为失效至少需要 2 个 Sentinel 同意)
sentinel down-after-milliseconds mymaster 60000 (指定了 Sentinel 认为服务器已经断线所需的毫秒数)
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1  在执行故障转移时, 最多可以有多少个从服务器同时对新的主服务器进行同步

2.2. 下线判断

  • 主观下线(Subjectively Down, 简称 SDOWN)指的是单个 Sentinel 实例对服务器做出的下线判断。
  • 客观下线(Objectively Down, 简称 ODOWN)指的是多个 Sentinel 实例在对同一个服务器做出 SDOWN 判断, 并且通过 SENTINEL is-master-down-by-addr 命令互相交流之后, 得出的服务器下线判断。 (一个 Sentinel 可以通过向另一个 Sentinel 发送 SENTINEL is-master-down-by-addr 命令来询问对方是否认为给定的服务器已下线。)

如果一个服务器没有在 master-down-after-milliseconds 选项所指定的时间内, 对向它发送 PING 命令的 Sentinel 返回一个有效回复(valid reply), 那么 Sentinel 就会将这个服务器标记为主观下线。

服务器对 PING 命令的有效回复可以是以下三种回复的其中一种:

返回 +PONG 。
返回 -LOADING 错误。
返回 -MASTERDOWN 错误。

如果服务器返回除以上三种回复之外的其他回复, 又或者在指定时间内没有回复 PING 命令, 那么 Sentinel 认为服务器返回的回复无效(non-valid)。
注意, 一个服务器必须在 master-down-after-milliseconds 毫秒内, 一直返回无效回复才会被 Sentinel 标记为主观下线。

客观下线条件只适用于主服务器: 对于任何其他类型的 Redis 实例, Sentinel 在将它们判断为下线前不需要进行协商, 所以从服务器或者其他 Sentinel 永远不会达到客观下线条件。

只要一个 Sentinel 发现某个主服务器进入了客观下线状态, 这个 Sentinel 就可能会被其他 Sentinel 推选出, 并对失效的主服务器执行自动故障迁移操作。

2.3. 自动故障迁移

一次故障转移操作由以下步骤组成:

发现主服务器已经进入客观下线状态。
对我们的当前纪元进行自增(详情请参考 Raft leader election ), 并尝试在这个纪元中当选。
如果当选失败, 那么在设定的故障迁移超时时间的两倍之后, 重新尝试当选。 如果当选成功, 那么执行以下步骤。
选出一个从服务器,并将它升级为主服务器。
向被选中的从服务器发送 SLAVEOF NO ONE 命令,让它转变为主服务器。
通过发布与订阅功能, 将更新后的配置传播给所有其他 Sentinel , 其他 Sentinel 对它们自己的配置进行更新。
向已下线主服务器的从服务器发送 SLAVEOF 命令, 让它们去复制新的主服务器。
当所有从服务器都已经开始复制新的主服务器时, 领头 Sentinel 终止这次故障迁移操作。

每当一个 Redis 实例被重新配置(reconfigured) —— 无论是被设置成主服务器、从服务器、又或者被设置成其他主服务器的从服务器 —— Sentinel 都会向被重新配置的实例发送一个 CONFIG REWRITE 命令, 从而确保这些配置会持久化在硬盘里。

Sentinel 使用以下规则来选择新的主服务器:

在失效主服务器属下的从服务器当中, 那些被标记为主观下线、已断线、或者最后一次回复 PING 命令的时间大于五秒钟的从服务器都会被淘汰。
在失效主服务器属下的从服务器当中, 那些与失效主服务器连接断开的时长超过 down-after 选项指定的时长十倍的从服务器都会被淘汰。
在经历了以上两轮淘汰之后剩下来的从服务器中, 我们选出复制偏移量(replication offset)最大的那个从服务器作为新的主服务器; 如果复制偏移量不可用, 或者从服务器的复制偏移量相同, 那么带有最小运行 ID 的那个从服务器成为新的主服务器。

min-replicas-max-lag-10 ()

3.集群模式

Redis-Cluster集群
Redis 集群是一个提供在多个Redis间节点间共享数据的程序集。
Redis 集群通过分区来提供一定程度的可用性,在实际环境中当某个节点宕机或者不可达的情况下继续处理命令.

优势:

自动分割数据到不同的节点上。
整个集群的部分节点失败或者不可达的情况下能够继续处理命令。

3.1. Redis 集群的数据分片

Redis 集群没有使用一致性hash, 而是引入了 哈希槽的概念.

Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽.集群的每个节点负责一部分hash槽,举个例子,比如当前集群有3个节点,那么:

节点 A 包含 0 到 5500号哈希槽.
节点 B 包含5501 到 11000 号哈希槽.
节点 C 包含11001 到 16384号哈希槽.

这种结构很容易添加或者删除节点。由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态.

3.2. Redis 集群的主从复制模型

为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有N-1个复制品.
A,B,C三个节点的集群,
我们为每个节点添加一个从节点A1,B1,C1
整个集群便有三个master节点和三个slave节点组成,这样在节点B失败后,集群便会选举B1为新的主节点继续服务,

3.3. Redis 一致性保证

Redis 并不能保证数据的强一致性. 这意味这在实际中集群在特定的条件下可能会丢失写操作.

第一个原因是因为集群是用了异步复制.
写操作过程:

客户端向主节点B写入一条命令.
主节点B向客户端回复命令状态.
主节点将写操作复制给他得从节点 B1, B2 和 B3.

3.4 分区

一种最简单的方法就是范围分区
需要建一张表存储数据到redis实例的映射关系
另一种可选的范围分区方案是散列分区(有一种比较高级的散列分区方法叫一致性哈希)

不同的分区实现方案
客户端分区就是在客户端就已经决定数据会被存储到哪个
代理分区
查询路由的意思是客户端随机地请求任意一个redis实例,然后由Redis将请求转发给正确的Redis节点。Redis Cluster实现了一种混合形式的查询路由,但并不是直接将请求从一个redis节点转发到另一个redis节点,而是在客户端的帮助下直接redirected到正确的redis节点。

持久化数据还是缓存
如果Redis被当做缓存使用,使用一致性哈希实现动态扩容缩容。
如果Redis被当做一个持久化存储使用,必须使用固定的keys-to-nodes映射关系,节点的数量一旦确定不能变化。否则的话(即Redis节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis集群可以做到这样

4. 分布式锁

算法只需具备3个特性就可以实现一个最低保障的分布式锁。

安全属性(Safety property): 独享(相互排斥)。在任意一个时刻,只有一个客户端持有锁。
活性A(Liveness property A): 无死锁。即便持有锁的客户端崩溃(crashed)或者网络被分裂(gets partitioned),锁仍然可以被获取。
活性B(Liveness property B): 容错。 只要大部分Redis节点都活着,客户端就可以获取和释放锁.

为什么基于故障转移的实现还不够
实现Redis分布式锁的最简单的方法就是在Redis中创建一个key,这个key有一个失效时间(TTL),以保证锁最终会被自动释放掉(这个对应特性2)。当客户端释放资源(解锁)的时候,会删除掉这个key。

从表面上看,似乎效果还不错,但是这里有一个问题:这个架构中存在一个严重的单点失败问题。如果Redis挂了怎么办?你可能会说,可以通过增加一个slave节点解决这个问题。但这通常是行不通的。这样做,我们不能实现资源的独享,因为Redis的主从同步通常是异步的。

客户端A从master获取到锁
在master将锁同步到slave之前,master宕掉了。
slave节点被晋级为master节点
客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。安全失效!

这里讨论的单实例加锁方法也是分布式加锁算法的基础。

获取锁使用命令:

SET resource_name my_random_value NX PX 30000

这个命令仅在不存在key的时候才能被执行成功(NX选项),并且这个key有一个30秒的自动失效时间(PX属性)。这个key的值是“my_random_value”(一个随机值),这个值在所有的客户端必须是唯一的,所有同一key的获取者(竞争者)这个值都不能一样。

value的值必须是随机数主要是为了更安全的释放锁,释放锁的时候使用脚本告诉Redis:只有key存在并且存储的值和我指定的值一样才能告诉我删除成功。可以通过以下Lua脚本实现:

if redis.call(“get”,KEYS[1]) == ARGV[1] then
return redis.call(“del”,KEYS[1])
else
return 0
end

使用这种方式释放锁可以避免删除别的客户端获取成功的锁。

5. Redlock算法

Redis的分布式环境中
为了取到锁,客户端应该执行以下操作:

获取当前Unix时间,以毫秒为单位。
依次尝试从N个实例,使用相同的key和随机值获取锁。在步骤2,当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个Redis实例。
客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功)。

失败时重试

当客户端无法取到锁时,应该在一个随机延迟后重试,防止多个客户端在同时抢夺同一资源的锁(这样会导致脑裂,没有人会取到锁)。同样,客户端取得大部分Redis实例锁所花费的时间越短,脑裂出现的概率就会越低(必要的重试),所以,理想情况一下,客户端应该同时(并发地)向所有Redis发送SET命令。

需要强调,当客户端从大多数Redis实例获取锁失败时,应该尽快地释放(部分)已经成功取到的锁,这样其他的客户端就不必非得等到锁过完“有效时间”才能取到(然而,如果已经存在网络分裂,客户端已经无法和Redis实例通信,此时就只能等待key的自动释放了,等于被惩罚了)。
释放锁

释放锁比较简单,向所有的Redis实例发送释放锁命令即可,不用关心之前有没有从Redis实例成功获取到锁.

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-18 18:32:07       50 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-18 18:32:07       54 阅读
  3. 在Django里面运行非项目文件

    2024-07-18 18:32:07       43 阅读
  4. Python语言-面向对象

    2024-07-18 18:32:07       54 阅读

热门阅读

  1. 大语言模型系列:Transformer

    2024-07-18 18:32:07       18 阅读
  2. SpringBoot日常:常用数据类型比较

    2024-07-18 18:32:07       17 阅读
  3. 如何查看Linux中某个项目是否在Docker中运行

    2024-07-18 18:32:07       16 阅读
  4. 如何发掘孩子的兴趣特长

    2024-07-18 18:32:07       16 阅读
  5. Oracle数据泵和RMAN异机备份还原速度对比

    2024-07-18 18:32:07       16 阅读
  6. 2024年对网络安全专业的观点解析

    2024-07-18 18:32:07       16 阅读
  7. uni-app 文件上传实战:适配多平台

    2024-07-18 18:32:07       15 阅读
  8. 每天一个数据分析题(四百三十二)- 假设检验

    2024-07-18 18:32:07       16 阅读
  9. NC65 设置下拉列表框值

    2024-07-18 18:32:07       19 阅读