Redis(9)——史上最强【集群】入门实践教程

1、Redis 集群概述

Redis 主从复制

目前 为止,咱们所学习的 Redis 都是 单机版 的,这也就意味着一旦咱们所依赖的 Redis 服务宕机了,咱们的主流程也会受到必定的影响,这固然是咱们不可以接受的。html

因此一开始咱们的想法是:搞一台备用机。这样咱们就能够在一台服务器出现问题的时候切换动态地到另外一台去:node

幸运的是,两个节点数据的同步咱们可使用 Redis 的 主从同步 功能帮助到咱们,这样一来,有个备份,内心就踏实多了。git

Redis 哨兵

后来由于某种神秘力量,Redis 老会在莫名其妙的时间点出问题 (好比半夜 2 点),我总不能 24 小时时刻守在电脑旁边切换节点吧,因而另外一个想法又开始了:给全部的节点找一个 "管家",自动帮我监听照顾节点的状态并切换:程序员

这大概就是 Redis 哨兵 (Sentinel) 的简单理解啦。什么?管家宕机了怎么办?相较于有大量请求的 Redis 服务来讲,管家宕机的几率就要小得多啦.. 若是真的宕机了,咱们也能够直接切换成当前可用的节点保证可用..github

Redis 集群化

好了,经过上面的一些解决方案咱们对 Redis 的 稳定性 稍微有了一些底气了,但单台节点的计算能力始终有限,所谓人多力量大,若是咱们把 多个节点组合一个可用的工做节点,那就大大增长了 Redis 的 高可用、可扩展、分布式、容错 等特性:web

2、主从复制

主从复制,是指将一台 Redis 服务器的数据,复制到其余的 Redis 服务器。前者称为 主节点(master),后者称为 从节点(slave)。且数据的复制是 单向 的,只能由主节点到从节点。Redis 主从复制支持 主从同步从从同步 两种,后者是 Redis 后续版本新增的功能,以减轻主节点的同步负担。redis

主从复制主要的做用

  • 数据冗余: 主从复制实现了数据的热备份,是持久化以外的一种数据冗余方式。
  • 故障恢复: 当主节点出现问题时,能够由从节点提供服务,实现快速的故障恢复 (其实是一种服务的冗余)
  • 负载均衡: 在主从复制的基础上,配合读写分离,能够由主节点提供写服务,由从节点提供读服务 (即写 Redis 数据时应用链接主节点,读 Redis 数据时应用链接从节点),分担服务器负载。尤为是在写少读多的场景下,经过多个从节点分担读负载,能够大大提升 Redis 服务器的并发量。
  • 高可用基石: 除了上述做用之外,主从复制仍是哨兵和集群可以实施的 基础,所以说主从复制是 Redis 高可用的基础。

快速体验

Redis 中,用户能够经过执行 SLAVEOF 命令或者设置 slaveof 选项,让一个服务器去复制另外一个服务器,如下三种方式是 彻底等效 的:算法

  • 配置文件:在从服务器的配置文件中加入: slaveof <masterip> <masterport>
  • 启动命令:redis-server 启动命令后加入 --slaveof <masterip> <masterport>
  • 客户端命令:Redis 服务器启动后,直接经过客户端执行命令: slaveof <masterip> <masterport>,让该 Redis 实例成为从节点。

须要注意的是:主从复制的开启,彻底是在从节点发起的,不须要咱们在主节点作任何事情。数组

第一步:本地启动两个节点

在正确安装好 Redis 以后,咱们可使用 redis-server --port <port> 的方式指定建立两个不一样端口的 Redis 实例,例如,下方我分别建立了一个 63796380 的两个 Redis 实例:安全

# 建立一个端口为 6379 的 Redis 实例
redis-server --port 6379
# 建立一个端口为 6380 的 Redis 实例
redis-server --port 6380
复制代码

此时两个 Redis 节点启动后,都默认为 主节点

第二步:创建复制

咱们在 6380 端口的节点中执行 slaveof 命令,使之变为从节点:

# 在 6380 端口的 Redis 实例中使用控制台
redis-cli -p 6380
# 成为本地 6379 端口实例的从节点
127.0.0.1:6380> SLAVEOF 127.0.0.1ø 6379
OK
复制代码

第三步:观察效果

下面咱们来验证一下,主节点的数据是否会复制到从节点之中:

  • 先在 从节点 中查询一个 不存在 的 key:
127.0.0.1:6380> GET mykey
(nil)
复制代码
  • 再在 主节点 中添加这个 key:
127.0.0.1:6379> SET mykey myvalue
OK
复制代码
  • 此时再从 从节点 中查询,会发现已经从 主节点 同步到 从节点
127.0.0.1:6380> GET mykey
"myvalue"
复制代码

第四步:断开复制

经过 slaveof <masterip> <masterport> 命令创建主从复制关系之后,能够经过 slaveof no one 断开。须要注意的是,从节点断开复制后,不会删除已有的数据,只是再也不接受主节点新的数据变化。

从节点执行 slaveof no one 以后,从节点和主节点分别打印日志以下:、

# 从节点打印日志
61496:M 17 Mar 2020 08:10:22.749 # Connection with master lost.
61496:M 17 Mar 2020 08:10:22.749 * Caching the disconnected master state.
61496:M 17 Mar 2020 08:10:22.749 * Discarding previously cached master state.
61496:M 17 Mar 2020 08:10:22.749 * MASTER MODE enabled (user request from 'id=4 addr=127.0.0.1:55096 fd=8 name= age=1664 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=34 qbuf-free=32734 obl=0 oll=0 omem=0 events=r cmd=slaveof')

# 主节点打印日志
61467:M 17 Mar 2020 08:10:22.749 # Connection with replica 127.0.0.1:6380 lost.
复制代码

实现原理简析

为了节省篇幅,我把主要的步骤都 浓缩 在了上图中,其实也能够 简化成三个阶段:准备阶段-数据同步阶段-命令传播阶段。下面咱们来进行一些必要的说明。

身份验证 | 主从复制安全问题

在上面的 快速体验 过程当中,你会发现 slaveof 这个命令竟然不须要验证?这意味着只要知道了 ip 和端口就能够随意拷贝服务器上的数据了?

那固然不可以了,咱们能够经过在 主节点 配置 requirepass 来设置密码,这样就必须在 从节点 中对应配置好 masterauth 参数 (与主节点 requirepass 保持一致) 才可以进行正常复制了。

SYNC 命令是一个很是耗费资源的操做

每次执行 SYNC 命令,主从服务器须要执行以下动做:

  1. 主服务器 须要执行 BGSAVE 命令来生成 RDB 文件,这个生成操做会 消耗 主服务器大量的 CPU、内存和磁盘 I/O 的资源
  2. 主服务器 须要将本身生成的 RDB 文件 发送给从服务器,这个发送操做会 消耗 主服务器 大量的网络资源 (带宽和流量),并对主服务器响应命令请求的时间产生影响;
  3. 接收到 RDB 文件的 从服务器 须要载入主服务器发来的 RBD 文件,而且在载入期间,从服务器 会由于阻塞而没办法处理命令请求

特别是当出现 断线重复制 的状况是时,为了让从服务器补足断线时确实的那一小部分数据,却要执行一次如此耗资源的 SYNC 命令,显然是不合理的。

PSYNC 命令的引入

因此在 Redis 2.8 中引入了 PSYNC 命令来代替 SYNC,它具备两种模式:

  1. 全量复制: 用于初次复制或其余没法进行部分复制的状况,将主节点中的全部数据都发送给从节点,是一个很是重型的操做;
  2. 部分复制: 用于网络中断等状况后的复制,只将 中断期间主节点执行的写命令 发送给从节点,与全量复制相比更加高效。 须要注意 的是,若是网络中断时间过长,致使主节点没有可以完整地保存中断期间执行的写命令,则没法进行部分复制,仍使用全量复制;

部分复制的原理主要是靠主从节点分别维护一个 复制偏移量,有了这个偏移量以后断线重连以后一比较,以后就能够仅仅把从服务器断线以后确实的这部分数据给补回来了。

更多的详细内容能够参考下方 参考资料 3

3、Redis Sentinel 哨兵

上图 展现了一个典型的哨兵架构图,它由两部分组成,哨兵节点和数据节点:

  • 哨兵节点: 哨兵系统由一个或多个哨兵节点组成,哨兵节点是特殊的 Redis 节点,不存储数据;
  • 数据节点: 主节点和从节点都是数据节点;

在复制的基础上,哨兵实现了 自动化的故障恢复 功能,下方是官方对于哨兵功能的描述:

  • 监控(Monitoring): 哨兵会不断地检查主节点和从节点是否运做正常。
  • 自动故障转移(Automatic failover):主节点 不能正常工做时,哨兵会开始 自动故障转移操做,它会将失效主节点的其中一个 从节点升级为新的主节点,并让其余从节点改成复制新的主节点。
  • 配置提供者(Configuration provider): 客户端在初始化时,经过链接哨兵来得到当前 Redis 服务的主节点地址。
  • 通知(Notification): 哨兵能够将故障转移的结果发送给客户端。

其中,监控和自动故障转移功能,使得哨兵能够及时发现主节点故障并完成转移。而配置提供者和通知功能,则须要在与客户端的交互中才能体现。

快速体验

第一步:建立主从节点配置文件并启动

正确安装好 Redis 以后,咱们去到 Redis 的安装目录 (mac 默认在 /usr/local/),找到 redis.conf 文件复制三份分别命名为 redis-master.conf/redis-slave1.conf/redis-slave2.conf,分别做为 1 个主节点和 2 个从节点的配置文件 (下图演示了我本机的 redis.conf 文件的位置)

打开能够看到这个 .conf 后缀的文件里面有不少说明的内容,所有删除而后分别改为下面的样子:

#redis-master.conf
port 6379
daemonize yes
logfile "6379.log"
dbfilename "dump-6379.rdb"

#redis-slave1.conf
port 6380
daemonize yes
logfile "6380.log"
dbfilename "dump-6380.rdb"
slaveof 127.0.0.1 6379

#redis-slave2.conf
port 6381
daemonize yes
logfile "6381.log"
dbfilename "dump-6381.rdb"
slaveof 127.0.0.1 6379
复制代码

而后咱们能够执行 redis-server <config file path> 来根据配置文件启动不一样的 Redis 实例,依次启动主从节点:

redis-server /usr/local/redis-5.0.3/redis-master.conf
redis-server /usr/local/redis-5.0.3/redis-slave1.conf
redis-server /usr/local/redis-5.0.3/redis-slave2.conf
复制代码

节点启动后,咱们执行 redis-cli 默认链接到咱们端口为 6379 的主节点执行 info Replication 检查一下主从状态是否正常:(能够看到下方正确地显示了两个从节点)

第二步:建立哨兵节点配置文件并启动

按照上面一样的方法,咱们给哨兵节点也建立三个配置文件。(哨兵节点本质上是特殊的 Redis 节点,因此配置几乎没什么差异,只是在端口上作区分就好)

# redis-sentinel-1.conf
port 26379
daemonize yes
logfile "26379.log"
sentinel monitor mymaster 127.0.0.1 6379 2

# redis-sentinel-2.conf
port 26380
daemonize yes
logfile "26380.log"
sentinel monitor mymaster 127.0.0.1 6379 2

# redis-sentinel-3.conf
port 26381
daemonize yes
logfile "26381.log"
sentinel monitor mymaster 127.0.0.1 6379 2
复制代码

其中,sentinel monitor mymaster 127.0.0.1 6379 2 配置的含义是:该哨兵节点监控 127.0.0.1:6379 这个主节点,该主节点的名称是 mymaster,最后的 2 的含义与主节点的故障断定有关:至少须要 2 个哨兵节点赞成,才能断定主节点故障并进行故障转移。

执行下方命令将哨兵节点启动起来:

redis-server /usr/local/redis-5.0.3/redis-sentinel-1.conf --sentinel
redis-server /usr/local/redis-5.0.3/redis-sentinel-2.conf --sentinel
redis-server /usr/local/redis-5.0.3/redis-sentinel-3.conf --sentinel
复制代码

使用 redis-cil 工具链接哨兵节点,并执行 info Sentinel 命令来查看是否已经在监视主节点了:

# 链接端口为 26379 的 Redis 节点
➜ ~ redis-cli -p 26379
127.0.0.1:26379> info Sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=2,sentinels=3
复制代码

此时你打开刚才写好的哨兵配置文件,你还会发现出现了一些变化:

第三步:演示故障转移

首先,咱们使用 kill -9 命令来杀掉主节点,同时 在哨兵节点中执行 info Sentinel 命令来观察故障节点的过程:

➜  ~ ps aux | grep 6379
longtao 74529 0.3 0.0 4346936 2132 ?? Ss 10:30上午 0:03.09 redis-server *:26379 [sentinel]
longtao 73541 0.2 0.0 4348072 2292 ?? Ss 10:18上午 0:04.79 redis-server *:6379
longtao 75521 0.0 0.0 4286728 728 s008 S+ 10:39上午 0:00.00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn 6379
longtao 74836 0.0 0.0 4289844 944 s006 S+ 10:32上午 0:00.01 redis-cli -p 26379
➜ ~ kill -9 73541
复制代码

若是 刚杀掉瞬间 在哨兵节点中执行 info 命令来查看,会发现主节点尚未切换过来,由于哨兵发现主节点故障并转移须要一段时间:

# 第一时间查看哨兵节点发现并未转移,还在 6379 端口
127.0.0.1:26379> info Sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=2,sentinels=3
复制代码

一段时间以后你再执行 info 命令,查看,你就会发现主节点已经切换成了 6381 端口的从节点:

# 过一段时间以后在执行,发现已经切换了 6381 端口
127.0.0.1:26379> info Sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=127.0.0.1:6381,slaves=2,sentinels=3
复制代码

但同时还能够发现,哨兵节点认为新的主节点仍然有两个从节点 (上方 slaves=2),这是由于哨兵在将 6381 切换成主节点的同时,将 6379 节点置为其从节点。虽然 6379 从节点已经挂掉,可是因为 哨兵并不会对从节点进行客观下线,所以认为该从节点一直存在。当 6379 节点从新启动后,会自动变成 6381 节点的从节点。

另外,在故障转移的阶段,哨兵和主从节点的配置文件都会被改写:

  • 对于主从节点: 主要是 slaveof 配置的变化,新的主节点没有了 slaveof 配置,其从节点则 slaveof 新的主节点。
  • 对于哨兵节点: 除了主从节点信息的变化,纪元(epoch) (记录当前集群状态的参数) 也会变化,纪元相关的参数都 +1 了。

客户端访问哨兵系统代码演示

上面咱们在 快速体验 中主要感觉到了服务端本身对于当前主从节点的自动化治理,下面咱们以 Java 代码为例,来演示一下客户端如何访问咱们的哨兵系统:

public static void testSentinel() throws Exception {
String masterName = "mymaster";
Set<String> sentinels = new HashSet<>();
sentinels.add("127.0.0.1:26379");
sentinels.add("127.0.0.1:26380");
sentinels.add("127.0.0.1:26381");

// 初始化过程作了不少工做
JedisSentinelPool pool = new JedisSentinelPool(masterName, sentinels);
Jedis jedis = pool.getResource();
jedis.set("key1", "value1");
pool.close();
}
复制代码

客户端原理

Jedis 客户端对哨兵提供了很好的支持。如上述代码所示,咱们只须要向 Jedis 提供哨兵节点集合和 masterName ,构造 JedisSentinelPool 对象,而后即可以像使用普通 Redis 链接池同样来使用了:经过 pool.getResource() 获取链接,执行具体的命令。

在整个过程当中,咱们的代码不须要显式的指定主节点的地址,就能够链接到主节点;代码中对故障转移没有任何体现,就能够在哨兵完成故障转移后自动的切换主节点。之因此能够作到这一点,是由于在 JedisSentinelPool 的构造器中,进行了相关的工做;主要包括如下两点:

  1. 遍历哨兵节点,获取主节点信息: 遍历哨兵节点,经过其中一个哨兵节点 + masterName 得到主节点的信息;该功能是经过调用哨兵节点的 sentinel get-master-addr-by-name 命令实现;
  2. 增长对哨兵的监听: 这样当发生故障转移时,客户端即可以收到哨兵的通知,从而完成主节点的切换。具体作法是:利用 Redis 提供的 发布订阅 功能,为每个哨兵节点开启一个单独的线程,订阅哨兵节点的 + switch-master 频道,当收到消息时,从新初始化链接池。

新的主服务器是怎样被挑选出来的?

故障转移操做的第一步 要作的就是在已下线主服务器属下的全部从服务器中,挑选出一个状态良好、数据完整的从服务器,而后向这个从服务器发送 slaveof no one 命令,将这个从服务器转换为主服务器。可是这个从服务器是怎么样被挑选出来的呢?

简单来讲 Sentinel 使用如下规则来选择新的主服务器:

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

4、Redis 集群

上图 展现了 Redis Cluster 典型的架构图,集群中的每个 Redis 节点都 互相两两相连,客户端任意 直连 到集群中的 任意一台,就能够对其余 Redis 节点进行 读写 的操做。

基本原理

Redis 集群中内置了 16384 个哈希槽。当客户端链接到 Redis 集群以后,会同时获得一份关于这个 集群的配置信息,当客户端具体对某一个 key 值进行操做时,会计算出它的一个 Hash 值,而后把结果对 16384 求余数,这样每一个 key 都会对应一个编号在 0-16383 之间的哈希槽,Redis 会根据节点数量 大体均等 的将哈希槽映射到不一样的节点。

再结合集群的配置信息就可以知道这个 key 值应该存储在哪个具体的 Redis 节点中,若是不属于本身管,那么就会使用一个特殊的 MOVED 命令来进行一个跳转,告诉客户端去链接这个节点以获取数据:

GET x
-MOVED 3999 127.0.0.1:6381
复制代码

MOVED 指令第一个参数 3999key 对应的槽位编号,后面是目标节点地址,MOVED 命令前面有一个减号,表示这是一个错误的消息。客户端在收到 MOVED 指令后,就当即纠正本地的 槽位映射表,那么下一次再访问 key 时就可以到正确的地方去获取了。

集群的主要做用

  1. 数据分区: 数据分区 (或称数据分片) 是集群最核心的功能。集群将数据分散到多个节点, 一方面 突破了 Redis 单机内存大小的限制, 存储容量大大增长另外一方面 每一个主节点均可以对外提供读服务和写服务, 大大提升了集群的响应能力。Redis 单机内存大小受限问题,在介绍持久化和主从复制时都有说起,例如,若是单机内存太大, bgsavebgrewriteaoffork 操做可能致使主进程阻塞,主从环境下主机切换时可能致使从节点长时间没法提供服务,全量复制阶段主节点的复制缓冲区可能溢出……
  2. 高可用: 集群支持主从复制和主节点的 自动故障转移 (与哨兵相似),当任一节点发生故障时,集群仍然能够对外提供服务。

快速体验

第一步:建立集群节点配置文件

首先咱们找一个地方建立一个名为 redis-cluster 的目录:

mkdir -p ~/Desktop/redis-cluster
复制代码

而后按照上面的方法,建立六个配置文件,分别命名为:redis_7000.conf/redis_7001.conf.....redis_7005.conf,而后根据不一样的端口号修改对应的端口值就行了:

# 后台执行
daemonize yes
# 端口号
port 7000
# 为每个集群节点指定一个 pid_file
pidfile ~/Desktop/redis-cluster/redis_7000.pid
# 启动集群模式
cluster-enabled yes
# 每个集群节点都有一个配置文件,这个文件是不能手动编辑的。确保每个集群节点的配置文件不通
cluster-config-file nodes-7000.conf
# 集群节点的超时时间,单位:ms,超时后集群会认为该节点失败
cluster-node-timeout 5000
# 最后将 appendonly 改为 yes(AOF 持久化)
appendonly yes
复制代码

记得把对应上述配置文件中根端口对应的配置都修改掉 (port/ pidfile/ cluster-config-file)

第二步:分别启动 6 个 Redis 实例

redis-server ~/Desktop/redis-cluster/redis_7000.conf
redis-server ~/Desktop/redis-cluster/redis_7001.conf
redis-server ~/Desktop/redis-cluster/redis_7002.conf
redis-server ~/Desktop/redis-cluster/redis_7003.conf
redis-server ~/Desktop/redis-cluster/redis_7004.conf
redis-server ~/Desktop/redis-cluster/redis_7005.conf
复制代码

而后执行 ps -ef | grep redis 查看是否启动成功:

能够看到 6 个 Redis 节点都以集群的方式成功启动了,可是如今每一个节点还处于独立的状态,也就是说它们每个都各自成了一个集群,尚未互相联系起来,咱们须要手动地把他们之间创建起联系。

第三步:创建集群

执行下列命令:

redis-cli --cluster create --cluster-replicas 1 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005
复制代码
  • 这里稍微解释一下这个 --replicas 1 的意思是:咱们但愿为集群中的每一个主节点建立一个从节点。

观察控制台输出:

看到 [OK] 的信息以后,就表示集群已经搭建成功了,能够看到,这里咱们正确地建立了三主三从的集群。

第四步:验证集群

咱们先使用 redic-cli 任意链接一个节点:

redis-cli -c -h 127.0.0.1 -p 7000
127.0.0.1:7000>
复制代码
  • -c表示集群模式; -h 指定 ip 地址; -p 指定端口。

而后随便 set 一些值观察控制台输入:

127.0.0.1:7000> SET name wmyskxz
-> Redirected to slot [5798] located at 127.0.0.1:7001
OK
127.0.0.1:7001>
复制代码

能够看到这里 Redis 自动帮咱们进行了 Redirected 操做跳转到了 7001 这个实例上。

咱们再使用 cluster info (查看集群信息)cluster nodes (查看节点列表) 来分别看看:(任意节点输入都可)

127.0.0.1:7001> CLUSTER INFO
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:2
cluster_stats_messages_ping_sent:1365
cluster_stats_messages_pong_sent:1358
cluster_stats_messages_meet_sent:4
cluster_stats_messages_sent:2727
cluster_stats_messages_ping_received:1357
cluster_stats_messages_pong_received:1369
cluster_stats_messages_meet_received:1
cluster_stats_messages_received:2727

127.0.0.1:7001> CLUSTER NODES
56a04742f36c6e84968cae871cd438935081e86f 127.0.0.1:7003@17003 slave 4ec8c022e9d546c9b51deb9d85f6cf867bf73db6 0 1584428884000 4 connected
4ec8c022e9d546c9b51deb9d85f6cf867bf73db6 127.0.0.1:7000@17000 master - 0 1584428884000 1 connected 0-5460
e2539c4398b8258d3f9ffa714bd778da107cb2cd 127.0.0.1:7005@17005 slave a3406db9ae7144d17eb7df5bffe8b70bb5dd06b8 0 1584428885222 6 connected
d31cd1f423ab1e1849cac01ae927e4b6950f55d9 127.0.0.1:7004@17004 slave 236cefaa9cdc295bc60a5bd1aed6a7152d4f384d 0 1584428884209 5 connected
236cefaa9cdc295bc60a5bd1aed6a7152d4f384d 127.0.0.1:7001@17001 myself,master - 0 1584428882000 2 connected 5461-10922
a3406db9ae7144d17eb7df5bffe8b70bb5dd06b8 127.0.0.1:7002@17002 master - 0 1584428884000 3 connected 10923-16383
127.0.0.1:7001>
复制代码

数据分区方案简析

方案一:哈希值 % 节点数

哈希取余分区思路很是简单:计算 key 的 hash 值,而后对节点数量进行取余,从而决定数据映射到哪一个节点上。

不过该方案最大的问题是,当新增或删减节点时,节点数量发生变化,系统中全部的数据都须要 从新计算映射关系,引起大规模数据迁移。

方案二:一致性哈希分区

一致性哈希算法将 整个哈希值空间 组织成一个虚拟的圆环,范围是 [0 - 232 - 1],对于每个数据,根据 key 计算 hash 值,确数据在环上的位置,而后今后位置沿顺时针行走,找到的第一台服务器就是其应该映射到的服务器:

与哈希取余分区相比,一致性哈希分区将 增减节点的影响限制在相邻节点。以上图为例,若是在 node1node2 之间增长 node5,则只有 node2 中的一部分数据会迁移到 node5;若是去掉 node2,则原 node2 中的数据只会迁移到 node4 中,只有 node4 会受影响。

一致性哈希分区的主要问题在于,当 节点数量较少 时,增长或删减节点,对单个节点的影响可能很大,形成数据的严重不平衡。仍是以上图为例,若是去掉 node2node4 中的数据由总数据的 1/4 左右变为 1/2 左右,与其余节点相比负载太高。

方案三:带有虚拟节点的一致性哈希分区

该方案在 一致性哈希分区的基础上,引入了 虚拟节点 的概念。Redis 集群使用的即是该方案,其中的虚拟节点称为 槽(slot)。槽是介于数据和实际节点之间的虚拟概念,每一个实际节点包含必定数量的槽,每一个槽包含哈希值在必定范围内的数据。

在使用了槽的一致性哈希分区中,槽是数据管理和迁移的基本单位。槽 解耦数据和实际节点 之间的关系,增长或删除节点对系统的影响很小。仍以上图为例,系统中有 4 个实际节点,假设为其分配 16 个槽(0-15);

  • 槽 0-3 位于 node1;4-7 位于 node2;以此类推....

若是此时删除 node2,只须要将槽 4-7 从新分配便可,例如槽 4-5 分配给 node1,槽 6 分配给 node3,槽 7 分配给 node4;能够看出删除 node2 后,数据在其余节点的分布仍然较为均衡。

节点通讯机制简析

集群的创建离不开节点之间的通讯,例如咱们上访在 快速体验 中刚启动六个集群节点以后经过 redis-cli 命令帮助咱们搭建起来了集群,实际上背后每一个集群之间的两两链接是经过了 CLUSTER MEET <ip> <port> 命令发送 MEET 消息完成的,下面咱们展开详细说说。

两个端口

哨兵系统 中,节点分为 数据节点哨兵节点:前者存储数据,后者实现额外的控制功能。在 集群 中,没有数据节点与非数据节点之分:全部的节点都存储数据,也都参与集群状态的维护。为此,集群中的每一个节点,都提供了两个 TCP 端口:

  • 普通端口: 即咱们在前面指定的端口 (7000等)。普通端口主要用于为客户端提供服务 (与单机节点相似);但在节点间数据迁移时也会使用。
  • 集群端口: 端口号是普通端口 + 10000 (10000是固定值,没法改变),如 7000 节点的集群端口为 17000集群端口只用于节点之间的通讯,如搭建集群、增减节点、故障转移等操做时节点间的通讯;不要使用客户端链接集群接口。为了保证集群能够正常工做,在配置防火墙时,要同时开启普通端口和集群端口。

Gossip 协议

节点间通讯,按照通讯协议能够分为几种类型:单对单、广播、Gossip 协议等。重点是广播和 Gossip 的对比。

  • 广播是指向集群内全部节点发送消息。 优势 是集群的收敛速度快(集群收敛是指集群内全部节点得到的集群信息是一致的), 缺点 是每条消息都要发送给全部节点,CPU、带宽等消耗较大。
  • Gossip 协议的特色是:在节点数量有限的网络中, 每一个节点都 “随机” 的与部分节点通讯 (并非真正的随机,而是根据特定的规则选择通讯的节点),通过一番杂乱无章的通讯,每一个节点的状态很快会达到一致。Gossip 协议的 优势 有负载 (比广播) 低、去中心化、容错性高 (由于通讯有冗余) 等; 缺点 主要是集群的收敛速度慢。

消息类型

集群中的节点采用 固定频率(每秒10次)定时任务 进行通讯相关的工做:判断是否须要发送消息及消息类型、肯定接收节点、发送消息等。若是集群状态发生了变化,如增减节点、槽状态变动,经过节点间的通讯,全部节点会很快得知整个集群的状态,使集群收敛。

节点间发送的消息主要分为 5 种:meet 消息ping 消息pong 消息fail 消息publish 消息。不一样的消息类型,通讯协议、发送的频率和时机、接收节点的选择等是不一样的:

  • MEET 消息: 在节点握手阶段,当节点收到客户端的 CLUSTER MEET 命令时,会向新加入的节点发送 MEET 消息,请求新节点加入到当前集群;新节点收到 MEET 消息后会回复一个 PONG 消息。
  • PING 消息: 集群里每一个节点每秒钟会选择部分节点发送 PING 消息,接收者收到消息后会回复一个 PONG 消息。 PING 消息的内容是自身节点和部分其余节点的状态信息,做用是彼此交换信息,以及检测节点是否在线。 PING 消息使用 Gossip 协议发送,接收节点的选择兼顾了收敛速度和带宽成本, 具体规则以下:(1)随机找 5 个节点,在其中选择最久没有通讯的 1 个节点;(2)扫描节点列表,选择最近一次收到 PONG 消息时间大于 cluster_node_timeout / 2 的全部节点,防止这些节点长时间未更新。
  • PONG消息: PONG 消息封装了自身状态数据。能够分为两种: 第一种 是在接到 MEET/PING 消息后回复的 PONG 消息; 第二种 是指节点向集群广播 PONG 消息,这样其余节点能够获知该节点的最新信息,例如故障恢复后新的主节点会广播 PONG 消息。
  • FAIL 消息: 当一个主节点判断另外一个主节点进入 FAIL 状态时,会向集群广播这一 FAIL 消息;接收节点会将这一 FAIL 消息保存起来,便于后续的判断。
  • PUBLISH 消息: 节点收到 PUBLISH 命令后,会先执行该命令,而后向集群广播这一消息,接收节点也会执行该 PUBLISH 命令。

数据结构简析

节点须要专门的数据结构来存储集群的状态。所谓集群的状态,是一个比较大的概念,包括:集群是否处于上线状态、集群中有哪些节点、节点是否可达、节点的主从状态、槽的分布……

节点为了存储集群状态而提供的数据结构中,最关键的是 clusterNodeclusterState 结构:前者记录了一个节点的状态,后者记录了集群做为一个总体的状态。

clusterNode 结构

clusterNode 结构保存了 一个节点的当前状态,包括建立时间、节点 id、ip 和端口号等。每一个节点都会用一个 clusterNode 结构记录本身的状态,并为集群内全部其余节点都建立一个 clusterNode 结构来记录节点状态。

下面列举了 clusterNode 的部分字段,并说明了字段的含义和做用:

typedef struct clusterNode {
//节点建立时间
mstime_t ctime;
//节点id
char name[REDIS_CLUSTER_NAMELEN];
//节点的ip和端口号
char ip[REDIS_IP_STR_LEN];
int port;
//节点标识:整型,每一个bit都表明了不一样状态,如节点的主从状态、是否在线、是否在握手等
int flags;
//配置纪元:故障转移时起做用,相似于哨兵的配置纪元
uint64_t configEpoch;
//槽在该节点中的分布:占用16384/8个字节,16384个比特;每一个比特对应一个槽:比特值为1,则该比特对应的槽在节点中;比特值为0,则该比特对应的槽不在节点中
unsigned char slots[16384/8];
//节点中槽的数量
int numslots;
…………
} clusterNode;
复制代码

除了上述字段,clusterNode 还包含节点链接、主从复制、故障发现和转移须要的信息等。

clusterState 结构

clusterState 结构保存了在当前节点视角下,集群所处的状态。主要字段包括:

typedef struct clusterState {
//自身节点
clusterNode *myself;
//配置纪元
uint64_t currentEpoch;
//集群状态:在线仍是下线
int state;
//集群中至少包含一个槽的节点数量
int size;
//哈希表,节点名称->clusterNode节点指针
dict *nodes;
//槽分布信息:数组的每一个元素都是一个指向clusterNode结构的指针;若是槽尚未分配给任何节点,则为NULL
clusterNode *slots[16384];
…………
} clusterState;
复制代码

除此以外,clusterState 还包括故障转移、槽迁移等须要的信息。

更多关于集群内容请自行阅读《Redis 设计与实现》,其中有更多细节方面的介绍 - redisbook.com/

相关阅读

  1. Redis(1)——5种基本数据结构 - www.wmyskxz.com/2020/02/28/…
  2. Redis(2)——跳跃表 - www.wmyskxz.com/2020/02/29/…
  3. Redis(3)——分布式锁深刻探究 - www.wmyskxz.com/2020/03/01/…
  4. Reids(4)——神奇的HyperLoglog解决统计问题 - www.wmyskxz.com/2020/03/02/…
  5. Redis(5)——亿级数据过滤和布隆过滤器 - www.wmyskxz.com/2020/03/11/…
  6. Redis(6)——GeoHash查找附近的人 www.wmyskxz.com/2020/03/12/…
  7. Redis(7)——持久化【一文了解】 - www.wmyskxz.com/2020/03/13/…
  8. Redis(8)——发布/订阅与Stream - www.wmyskxz.com/2020/03/15/…

参考资料

  1. 《Redis 设计与实现》 | 黄健宏 著 - redisbook.com/
  2. 《Redis 深度历险》 | 钱文品 著 - book.douban.com/subject/303…
  3. 深刻学习Redis(3):主从复制 - www.cnblogs.com/kismetv/p/9…
  4. Redis 主从复制 原理与用法 - blog.csdn.net/Stubborn_Co…
  5. 深刻学习Redis(4):哨兵 - www.cnblogs.com/kismetv/p/9…
  6. Redis 5 以后版本的高可用集群搭建 - www.jianshu.com/p/8045b92fa…
  • 本文已收录至个人 Github 程序员成长系列 【More Than Java】,学习,不止 Code,欢迎 star:github.com/wmyskxz/Mor…
  • 我的公众号 :wmyskxz, 我的独立域名博客:wmyskxz.com,坚持原创输出,下方扫码关注,2020,与您共同成长!

很是感谢各位人才能 看到这里,若是以为本篇文章写得不错,以为 「我没有三颗心脏」有点东西 的话,求点赞,求关注,求分享,求留言!

创做不易,各位的支持和承认,就是我创做的最大动力,咱们下篇文章见!

本文使用 mdnice 排版

相关文章
相关标签/搜索