redis入门指南之集群

重要星级 ★★★★★

集群

即使使用哨兵,此时的Redis集群的每个数据库依然存有集群中的所有数据,从而导致集群的总数据存储量受限于可用存储内存最小的数据库节点,形成木桶效应。由于Redis中的所有数据都是基于内存存储,这一问题就尤为突出了,尤其是当使用Redis做了持久化存储服务使用时。
对Redis进行水平扩容,在旧版Redis中通常使用客户端分片来解决这个问题,即启动多个Redis数据库节点,由客户端决定每个键交由哪个数据库节点存储,下次客户端读取该键值时直接到该节点读取。这样可以实现将整个数据分布存储在N个数据库节点中,每个节点只存放总数据量的1/N。但对于需要扩容的场景来说,在客户端分片后,如果想增加更多的节点,就需要对数据进行手工迁移,同时在迁移的过程中为了保证数据的一致性,还需要将集群暂时下线,相对比较复杂。
考虑到Redis实例非常轻量的特点,可以采用预分片技术(presharding)来在一定程度上避免此问题,具体来说是在节点部署初期,就提前考虑日后的存储规模,建立足够多的实例(如128个节点),初期数据很少,所以每个节点存储的数据也非常少,但由于节点轻量的特性,数据之外的内存开销并不大,这使得只需要很少的服务器即可运行这些实例。日后存储规模扩大后,所要做的不过是将某些实例迁移到其他服务器上,而不需要对所有数据进行重新分片并进行集群下线和数据迁移了。
无论如何,客户端分片终归是有非常多的缺点,比如维护成本高,增加、移除节点较繁琐等。Redis3.0版的一大特性就是支持集群(Cluster,注意与本章标题–广义的“集群”相区别)功能。集群的特点在于拥有和单机实例同样的性能,同时在网络分区后能够提供一定的可访问性以及对主数据库故障恢复的支持。另外集群支持几乎所有的单机实例支持的命令,对于涉及多键的命令(如MGET),如果每个键都位于同一个节点中,则可以正常支持,否则会提示错误。除此之外集群还有一个限制是只能使用默认的0号数据库,如果执行SELECT切换数据库则会提示错误。
哨兵与集群是两个独立的功能,但从特性来看哨兵可以视为集群的子集,当不需要数据分片或者已经在客户端进行分片的场景下哨兵就足够使用了,但如果需要进行水平扩容,则集群是一个非常好的选择。

1、配置集群

使用集群,只需要将每个数据库节点的cluster-enabled配置选项打开即可。每个集群中至少需要3个主数据库才能正常运行。
为了演示集群的应用场景以及故障恢复等操作,这里以配置一个3主3从的集群系统为例。首先建立启动6个Redis实例,需要注意的是配置文件应该打开cluster-enabled。一个示例配置为:

port 6380
cluster-enabled yes  

其中port参数修改成实际的端口即可。这里假设6个实例的端口分别是6380、6381、6382、6383、6384和6385。集群会将当前节点记录的集群状态持久化地存储在指定文件中,这个文件默认为当前工作目录下的nodes.conf文件。每个节点对应的文件必须不同,否则会造成启动失败,所以启动节点时要注意最后为每个节点使用不同的工作目录,或者是通过cluster-config-file选项修改持久化文件的名称:

cluster-config-file nodes.conf  

启动后,可以使用Redis命令行客户端连接任意一个节点使用INFO命令来判断集群是否正常启用了:

192.168.100.238:6381> info cluster
# Cluster
cluster_enabled:1

其中cluster_enabled为1表示集群正常启用了。现在每个节点都是完全独立的,要将它们加入同一个集群中还需要几个步骤。Redis源代码中提供了一个辅助工具redis-trib.rb可以非常方便地完成这一任务。因为redis-trib.rb使用Ruby语言编写的,所以运行前需要在服务器上安装Ruby程序,具体安装方法请查阅相关文档。redis-trib.rb依赖于gem包redis,可执行gem install redis来安装。使用redis-trib.rb来初始化集群,只需要执行:

[admin@KFCS3 redis-stable_01]$ ./src/redis-trib.rb create --replicas 1 127.0.0.1:6380 
127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 127.0.0.1:6385

其中create参数表示要初始化集群,–replicas 1表示每个主数据库拥有的从数据库个数为1,所以集群共有3(6/2)个主数据库以及3个从数据库。执行完成后,redis-trib.rb会输出如下内容:

--未执行集群时
>>> Creating cluster
>>> Performing hash slots allocation on 6 nodes...
Using 3 masters:
127.0.0.1:6380
127.0.0.1:6381
127.0.0.1:6382
Adding replica 127.0.0.1:6383 to 127.0.0.1:6380
Adding replica 127.0.0.1:6384 to 127.0.0.1:6381
Adding replica 127.0.0.1:6385 to 127.0.0.1:6382
M: bec0f2a8743b6636cf53cd5611137dd9a5ee72f3 127.0.0.1:6380
   slots:0-5460 (5461 slots) master
M: a24810378b2ee15efbbef8bbf285872198ef6902 127.0.0.1:6381
   slots:5461-10922 (5462 slots) master
M: e18b9946f5c4db8b170b395a6de8204ab56237f9 127.0.0.1:6382
   slots:10923-16383 (5461 slots) master
S: 50c78a069f7d4a052d0f3e4c83083e6279b49acb 127.0.0.1:6383
   replicates bec0f2a8743b6636cf53cd5611137dd9a5ee72f3
S: 6054c3053a0f50a9b8f6dc2e9134d6b1ce25e90c 127.0.0.1:6384
   replicates a24810378b2ee15efbbef8bbf285872198ef6902
S: 4c9ec5e346ac451d6d44df4d817112fdac3f6da6 127.0.0.1:6385
   replicates e18b9946f5c4db8b170b395a6de8204ab56237f9
Can I set the above configuration? (type 'yes' to accept): yes
--已执行过集群后
[admin@KFCS3 redis-stable]$ ./src/redis-trib.rb create --replicas 1 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 127.0.0.1:6385
>>> Creating cluster
[ERR] Node 127.0.0.1:6382 is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0.

上实例2表示节点已分配插槽。上例1输出内容包括集群具体的分配方案,如果觉得没问题则输入yes来开始创建。下面根据上面的输出详细讲解集群创建的过程。
首先redis-trib.rb会以客户端的形式尝试连接所有节点,并发送PING命令以确定节点能够正常服务。如果有任何节点无法连接,则创建失败。同时发送INFO命令获取每个节点的运行ID以及是否开启了集群功能(即cluster_enabled为1)。
准备就绪后集群会向每个节点发送CLUSTER MEET 命令,格式为CLUSTER MEET ip port,这个命令用来告诉当前节点指定ip和port上在运行的节点也是集群的的一部分,从而使得6个节点最终可以纳入一个集群。
然后redis-trib.rb会分配主从数据库节点,分配的原则是尽量保证每个主数据库运行在不同的IP地址上,同时每个从数据库和主数据库均不运行在同一个IP地址上,以保证系统的容灾能力。分配结果如下:

Using 3 masters:
127.0.0.1:6380
127.0.0.1:6381
127.0.0.1:6382
Adding replica 127.0.0.1:6383 to 127.0.0.1:6380
Adding replica 127.0.0.1:6384 to 127.0.0.1:6381
Adding replica 127.0.0.1:6385 to 127.0.0.1:6382

其中主数据库是6380、6381和6382端口上的节点(以下使用端口来指代节点),6383是6380的从数据库,6384是6381的从数据库,6385是6382的从数据库。分配完成后,会为每个主数据库分配插槽,分配插槽的过程其实就是分配哪些键归哪些节点负责。之后对每个要成为子数据库的节点发送CLUSTER REPLICATE 主数据库的运行ID 来讲当前节点转换成从数据库并复制指定运行ID的节点(主数据库)。
此时整个集群的过程即创建完毕,使用Redis命令行客户端连接任意一个节点执行CLUSTER NODES 可以获得集群中的所有节点信息,如在6380执行:

127.0.0.1:6380> cluster nodes
50c78a069f7d4a052d0f3e4c83083e6279b49acb 127.0.0.1:6383 slave bec0f2a8743b6636cf53cd5611137dd9a5ee72f3 0 1496677290597 4 connected
e18b9946f5c4db8b170b395a6de8204ab56237f9 127.0.0.1:6382 master - 0 1496677293602 3 connected 10923-16383
6054c3053a0f50a9b8f6dc2e9134d6b1ce25e90c 127.0.0.1:6384 slave a24810378b2ee15efbbef8bbf285872198ef6902 0 1496677294603 5 connected
bec0f2a8743b6636cf53cd5611137dd9a5ee72f3 127.0.0.1:6380 myself,master - 0 0 1 connected 0-5460
4c9ec5e346ac451d6d44df4d817112fdac3f6da6 127.0.0.1:6385 slave e18b9946f5c4db8b170b395a6de8204ab56237f9 0 1496677292600 6 connected
a24810378b2ee15efbbef8bbf285872198ef6902 127.0.0.1:6381 master - 0 1496677295605 2 connected 5461-10922

从上面的输出中可以看到所有节点的运行ID、地址和端口、角色、状态以及负责的插槽等信息,后文会进行解读。redis-trib.rb是一个非常好用的辅助工具,其本质是通过执行Redis命令来实现集群管理的任务。如果有兴趣可以尝试不借助redis-trib.rb,手动建立一次集群。

2、节点的增加

前面介绍过redis-trib.rb是使用CLUSTER MEET 命令来使每个节点认识集群中的其他节点的,可想而知如果要向集群中加入新节点,也需要使用CLUSTER MEET 命令实现。加入新节点非常简单,只需要向新节点(以下记着A)发送如下命令:

CLUSTER MEET ip port  

ip和port是集群中任意一个节点的地址和端口号,A接受到客户端发来的命令后,会与该地址和端口号的节点B进行握手,使B将A认作当前集群中的一员。当B与A握手成功后,B会使用Gossip协议将节点A的信息通知给集群中的每一个节点。通过这一方式,即使集群中有多个节点,也只需要选择MEET 其中任意一个节点,即可使新节点最终加入整个集群。

3、插槽的分配

新的节点加入集群后有两种选择,要么使用CLUSTER REPLICATE 命令复制每个数据库来以从数据库的形式运行,要么向集群申请分配插槽(solt)来以主数据库的形式运行。在一个集群中,所有的键会被分配给16384个插槽,而每个主数据库会负责处理其中的一部分插槽。现在回头来看上面创建集群时的输出:

M: bec0f2a8743b6636cf53cd5611137dd9a5ee72f3 127.0.0.1:6380
   slots:0-5460 (5461 slots) master
M: a24810378b2ee15efbbef8bbf285872198ef6902 127.0.0.1:6381
   slots:5461-10922 (5462 slots) master
M: e18b9946f5c4db8b170b395a6de8204ab56237f9 127.0.0.1:6382
   slots:10923-16383 (5461 slots) master

上面的每一行表示一个主数据库的信息,其中可以看到6380负责处理0~5460折5461个插槽,6381负责处理5461~10922这5462个插槽,6382负责处理10923~16383这5461个插槽。虽然redis-trib.rb初始化集群分配给每个节点的插槽都是连续的,但是实际上Redis并没有限制,可以将任意的几个插槽分配给任意的节点负责。
在介绍如何将插槽分配给指定的节点前,先来介绍键和插槽的对应关系。Redis将每个键的键名的有效部分使用CRC16算法计算出散列值,然后取对16384的余数。这样使得每个键都可以分配到16384个插槽中,进而分配的指定的一个节点中处理。这里键名的有效部分是指:
(1)如果键名包含{符号,且在{符合后面存在}符号,并且{和}之间有至少一个字符,则有效部分是指{和}之间的内容。
(2)如果不满足上一条规则,那么整个键名为有效部分。
例如,键hello.world的有效部分为“hello.world”,键{user102}:last.name的有效部分为“user102”。如本节引言所说,如果命令涉及多个键(如MGET),只有当所有键都位于同一个节点时Redis才能正常支持。利用键的分配规则,可以将所有相关的键的有效部分设置成相同的值使得相关键都能分配到同一个节点以支持多键操作。比如,{user102}:first.name和{user102}:last.name会被分配到同一个节点,所以可以使用MGET {user102}:first.name {user102}:last.name 来同时获取两个键的值。介绍完键与插槽的对应关系后,接下来再来介绍如何将插槽分配给指定节点。插槽的分配分为如下几种情况。
(1)插槽之前没有被分配过,现在想分配给指定节点。
(2)插槽之前被分配过,现在想移动到指定节点。
其中第一种情况使用CLUSTER ADDSLOTS命令来实现,redis-trib.rb也是通过该命令在创建集群时为新节点分配插槽的。CLUSTER ADDSLOTS命令的用法为:

CLUSTER ADDSLOTS  slot1 [slot2] ... [slotN]

如想将100和101两个插槽分配给某个节点,只需要在该节点执行:CLUSTER ADDSLOTS 100 101即可。如果指定插槽已经分配过了,则会提示:

127.0.0.1:6380> cluster addslots 100 101
(error) ERR Slot 100 is already busy  

可以通过命令 CLUSTER SLOTS 来查看插槽分配情况,如:

127.0.0.1:6380> cluster slots
1) 1) (integer) 10923
   2) (integer) 16383
   3) 1) "127.0.0.1"
      2) (integer) 6382
      3) "e18b9946f5c4db8b170b395a6de8204ab56237f9"
   4) 1) "127.0.0.1"
      2) (integer) 6385
      3) "4c9ec5e346ac451d6d44df4d817112fdac3f6da6"
2) 1) (integer) 0
   2) (integer) 5460
   3) 1) "127.0.0.1"
      2) (integer) 6380
      3) "bec0f2a8743b6636cf53cd5611137dd9a5ee72f3"
   4) 1) "127.0.0.1"
      2) (integer) 6383
      3) "50c78a069f7d4a052d0f3e4c83083e6279b49acb"
3) 1) (integer) 5461
   2) (integer) 10922
   3) 1) "127.0.0.1"
      2) (integer) 6381
      3) "a24810378b2ee15efbbef8bbf285872198ef6902"
   4) 1) "127.0.0.1"
      2) (integer) 6384
      3) "6054c3053a0f50a9b8f6dc2e9134d6b1ce25e90c"  

其中返回结果的格式很容易理解,一共3条记录,每条记录的前两个表示插槽的开始号码和结束号码,后面的值则为负责该插槽的节点,包括主数据库和所有的从数据库,主数据库始终在第一位。
对于情况2,处理起来就相对复杂一些,不过redis-trib.rb提供了比较方便的方式来对插槽进行迁移。我们首先使用redis-trib.rb将一个插槽从6380迁移到6381,然后再介绍如何不使用redis-trib.rb来完成迁移。首先执行如下命令:

[admin@KFCS3 redis-stable_01]$ ./src/redis-trib.rb reshard 127.0.0.1:6380

其中reshard表示告诉redis-trib.rb要重新分片,127.0.0.1:6380是集群中任意一个节点的地址和端口,redis-trib.rb会自动获取集群信息。接下来redis-trib.rb将会询问具体如何进行重新分片,首先会询问想要迁移多少个插槽:

How many slots do you want to move (from 1 to 16384)?

我们只需要迁移一个,所以输入1后回车。接下来redis-trib.rb会询问要把插槽迁移到哪个节点:

How many slots do you want to move (from 1 to 16384)? 1
What is the receiving node ID? 

可以通过CLUSTER NODES命令获取6381的运行ID,这里是a24810378b2ee15efbbef8bbf285872198ef6902,输入并回车。接着最后异步是询问从那个节点移除插槽:

What is the receiving node ID? a24810378b2ee15efbbef8bbf285872198ef6902
Please enter all the source node IDs.
  Type 'all' to use all the nodes as source nodes for the hash slots.
  Type 'done' once you entered all the source nodes IDs.
Source node #1:all

我们输入6380对应的ID按回车后输入done再按回车确认即可。接下来输入yes来确认重新分片方案,重新分片即告成功。

Ready to move 1 slots.
  Source nodes:
    M: bec0f2a8743b6636cf53cd5611137dd9a5ee72f3 127.0.0.1:6380
   slots:0-5460 (5461 slots) master
   1 additional replica(s)
    M: e18b9946f5c4db8b170b395a6de8204ab56237f9 127.0.0.1:6382
   slots:10923-16383 (5461 slots) master
   1 additional replica(s)
  Destination node:
    M: a24810378b2ee15efbbef8bbf285872198ef6902 127.0.0.1:6381
   slots:5461-10922 (5462 slots) master
   1 additional replica(s)
  Resharding plan:
    Moving slot 0 from bec0f2a8743b6636cf53cd5611137dd9a5ee72f3
Do you want to proceed with the proposed reshard plan (yes/no)? yes
Moving slot 0 from 127.0.0.1:6380 to 127.0.0.1:6381:

使用CLUSTER SLOTS命令获取当前插槽的分配情况如下:

127.0.0.1:6380> cluster slots
1) 1) (integer) 10923
   2) (integer) 16383
   3) 1) "127.0.0.1"
      2) (integer) 6382
      3) "e18b9946f5c4db8b170b395a6de8204ab56237f9"
   4) 1) "127.0.0.1"
      2) (integer) 6385
      3) "4c9ec5e346ac451d6d44df4d817112fdac3f6da6"
2) 1) (integer) 1
   2) (integer) 5460
   3) 1) "127.0.0.1"
      2) (integer) 6380
      3) "bec0f2a8743b6636cf53cd5611137dd9a5ee72f3"
   4) 1) "127.0.0.1"
      2) (integer) 6383
      3) "50c78a069f7d4a052d0f3e4c83083e6279b49acb"
3) 1) (integer) 0--多出来的第0号插槽
   2) (integer) 0
   3) 1) "127.0.0.1"
      2) (integer) 6381
      3) "a24810378b2ee15efbbef8bbf285872198ef6902"
   4) 1) "127.0.0.1"
      2) (integer) 6384
      3) "6054c3053a0f50a9b8f6dc2e9134d6b1ce25e90c"
4) 1) (integer) 5461
   2) (integer) 10922
   3) 1) "127.0.0.1"
      2) (integer) 6381
      3) "a24810378b2ee15efbbef8bbf285872198ef6902"
   4) 1) "127.0.0.1"
      2) (integer) 6384
      3) "6054c3053a0f50a9b8f6dc2e9134d6b1ce25e90c"

可以看到现在比之前多了一条记录,第0号插槽已经有6381负责,此时重新分片成功。那么redis-trib.rb实现重新分片的原理是什么呢?我们不妨不借助redis-trib.rb手工进行重新分片,使用如下命令即可:

CLUSTER SETSLOTS 插槽号 NODE 新节点的运行ID

如想要把0号插槽迁移回6380:

127.0.0.1:6380> cluster nodes
50c78a069f7d4a052d0f3e4c83083e6279b49acb 127.0.0.1:6383 slave bec0f2a8743b6636cf53cd5611137dd9a5ee72f3 0 1496714712675 4 connected
e18b9946f5c4db8b170b395a6de8204ab56237f9 127.0.0.1:6382 master - 0 1496714710672 3 connected 10923-16383
6054c3053a0f50a9b8f6dc2e9134d6b1ce25e90c 127.0.0.1:6384 slave a24810378b2ee15efbbef8bbf285872198ef6902 0 1496714711674 7 connected
bec0f2a8743b6636cf53cd5611137dd9a5ee72f3 127.0.0.1:6380 myself,master - 0 0 1 connected 1-5460
4c9ec5e346ac451d6d44df4d817112fdac3f6da6 127.0.0.1:6385 slave e18b9946f5c4db8b170b395a6de8204ab56237f9 0 1496714710172 6 connected
a24810378b2ee15efbbef8bbf285872198ef6902 127.0.0.1:6381 master - 0 1496714706668 7 connected 0 5461-10922
127.0.0.1:6380> cluster setslot 0 node bec0f2a8743b6636cf53cd5611137dd9a5ee72f3
OK

此时重新使用CLUSTER SLOTS 查看插槽的分配情况,可以看到已经恢复如初了。然而这样迁移插槽的前提是插槽中并没有任何键,因为使用CLUSTER SETSLOT 命令迁移插槽时并不会连同相应的键一起迁移,这就造成了客户端在指定节点无法找到未迁移的键,造成这些键对客户端来说“丢失了”,为此需要手工获取插槽中存储在哪些键,然后将每个键迁移到新的节点中才行。手工获取某个插槽存在哪些键的方法是:

CLUSTER GETKEYSINSLOT 插槽号 要返回的键的数量

之后对每个键,使用MIGRATE命令将其迁移到目标节点:

MIGRATE 目标节点地址 目标节点端口 键名 数据库号码 超时时间 [COPY] [REPLACE]

其中COPY选项表示不将键从当前数据库中删除,而是复制一份副本。REPLACE表示如果目标节点存在同名键,则覆盖。因为集群模式只能使用0号数据库,所以数据库号码始终未0。如要把键abc从当前节点(如6381)迁移到6380:

MIGRATE 127.0.0.1 127.0.0.1 6380 abc 0 15999 REPLACE

至此,我们已经知道如果将插槽委派给其他节点,并同时将当前节点中的插槽下所有的键迁移到目标节点中。然而还有最后一个是如果要迁移的数据量比较大,整个过程会话费较长时间,那么究竟在什么时候执行 CLUSTER SETSLOT 命令来完成插槽的交接呢?如果在键迁移未完成时执行,那么客户端就会尝试在新的节点读取键值,此时还没迁移完成,自然有可能读取不到键值,从而造成相关键的临时“丢失”。相反,如果在键迁移完成后在执行,那么在迁移时客户端会在旧的节点读取键值,然后有些键已经迁移到新节点上了,同样也会造成键的临时“丢失”。那么redis-trib.rb工具是如何解决这个问题的呢?Redis提供了如下两个命令用来实现在集群不下线的情况下迁移数据:

CLUSTER SETSLOT 插槽号 MIGRATING 新节点的运行ID
CLUSTER SETSLOT 插槽号 IMPORTING 原节点的运行ID

进行迁移时,假设要把0号插槽从A迁移到B,此时redis-trib.rb会依次执行如下操作。
(1)在B执行CLUSTER SETSLOT 0 IMPORTING A。
(2)在A执行CLUSTER SETSLOT 0 MIGRATING B。
(3)执行CLUSTER GETKEYSINSLOT 0 获取0号插槽的键列表。
(4)对第3步获取的每个键执行MIGRATE命令,将其从A迁移到B。
(5)执行CLUSTER SETSLOT 0 NODE B 来完成迁移。
从上面的步骤来看redis-trib.rb多了1和2两个步骤,这两个步骤就是为了解决迁移过程中键的临时“丢失”问题。首先执行完前两步后,当客户端向A请求插槽0中的键时,如果键存在(即尚未被迁移),则正常处理,如果不存在,则返回一个ASK跳转请求,告诉客户端这个键在B里,如下图所示,客户端接受到ASK跳转请求后,首先向B发送ASKING命令,然后再重新发送之前的命令。

客户端请求A节点,服务器返回ASK情况

相反,当客户端向B请求插槽0中的键,如果前面执行了ASKING命令,则返回键值内容,否则返回MOVED跳转请求,如下图所示,这样一来客户端只有能够处理ASK跳转,则可以在数据库迁移时自动从正确的节点获取到相应的键值,避免了键在迁移过程中临时“丢失”的问题。

客户端请求B节点,根据是否前面执行过ASKING,则返回不同

4、获取与插槽对应的节点

对于指定的键,可以根据前面讲述的算法来计算其属于哪个插槽,但是如何获取某一个键有哪一个节点负责呢?
实际上,当客户端向集群中的任意一个节点发送命令后,该节点会判断相应的键是否在当前节点中,如果键在该节点中,则会像单机实例一样正确处理该命令;如果键不在该节点中,就会返回一个MOVE重定向请求,告诉客户端这个键目前由哪个节点负责,然后客户端再将同样的请求项目表节点重新发送一次以获取结果。
一些语言的redis库支持代理MOVE请求,所以对于开发者而言命令重定向的过程是透明的,使用集群与使用单机实例并没有什么不同。然而也有些语言库并不支持集群,这时就需要在客户端编码处理了。
还是以上面的集群配置为例,键foo实际应该由6382节点负责,如果尝试在6380节点执行与键foo相关的命令,就会有如下输出:

127.0.0.1:6380> set foo bar
(error) MOVED 12182 127.0.0.1:6382

返回的是一个MOVE重定向请求,12182表示foo所属的插槽号,127.0.0.1:6382则是负责该插槽的节点地址和端口,客户端收到重定向请求后,应该将命令重新向6382节点发送一次:

127.0.0.1:6382> set foo bar
OK

Redis命令行客户端提供了集群模式来支持自动重定向,使用-c参数来启用:

[admin@KFCS3 redis-stable_01]$ ./src/redis-cli -p 6380
127.0.0.1:6380> get foo
(error) MOVED 12182 127.0.0.1:6382
--加了c参数之后
[admin@KFCS3 redis-stable_01]$ ./src/redis-cli -c -p 6380
127.0.0.1:6380> get foo
-> Redirected to slot [12182] located at 127.0.0.1:6382
"bar"

可见加入了-c参数后,如果当前节点并不负责要处理的键,Redis命令行客户端会进行自动命令重定向。而这一过程正是每个支持集群的客户端应该实现的。
然而相比单机实例,集群的命令重定向也增加了命令的请求次数,原先只需要执行一次的命令现在有可能需要依次发向两个节点,算上往返时延,可以说请求重定向对性能还是有些影响的。
为了解决这一问题,当发现新的重定向请求时,客户端应该在重新向正确节点发送命令的同时,缓存插槽的路由信息,即记录下当前插槽时由哪个节点负责的。这样每次发起命令时,客户端首先计算相关键是属于哪个插槽的,然后根据缓存的路由判断插槽有哪个节点负责。考虑到插槽总数相对少(16384个),缓存所有插槽的路由信息后,每次命令将均只发向正确的节点,从而达到和单机实例同样的性能。

5、故障恢复

在一个集群中,每个节点都会定期向其他节点发送PING命令,并通过有没有收到回复来判断目标节点是否已经下线了。具体来说,集群中的每个节点每隔1秒钟就会随机选择5个节点,然后选择其中最久没有响应的节点发送PING命令。
如果一定时间内目标节点没有响应回复,则发送PING命令的节点会认为目标节点疑似下线(PFALL)。疑似下线可以与哨兵的主观下线类比,两者都表示某一节点从自身的角度认为目标节点时下线状态。需要一定数量的节点都认为该节点疑似下线才可以,这一过程具体为:
(1)一旦节点A认为节点B是疑似下线状态,就会在集群中传播该消息,所有其他节点收到消息后都会记录下这一信息;
(2)当集群中的某一节点C收集到半数以上的节点认为B是疑似下线的状态时,就会将B标记为下线(FALL),并且向集群中的其他节点传播该消息,从而使得B在整个集群中下线。
在集群中,当一个数据库下线时,就会出现一部分插槽无法写入的问题。这时如果该主数据库拥有至少一个从数据库,集群就进行故障恢复操作来将其中一个从数据库转变成主数据库来保证集群的完整。选择哪个从数据库来作为主数据库的过程与哨兵中选择领头哨兵的过程一样,都是基于Raft算法,过程如下:
(1)发现其复制的主数据下线的从数据库(下面称作A)向每个集群中的节点发送请求,要求对方选自己成为主数据库。
(2)如果收到请求的节点没有选过其他人,则会同意将A设置成主数据库。
(3)如果A发现有超过集群中节点总数一半的节点同意选自己成为主数据库,则A则成为主数据库。
(4)当有多个从数据库节点同时参选主数据库,则会出现没有任何节点当选的可能。此时每个参选节点将等待一个随机时间重新发起参选请求,进行下一轮选举,直到选举成功。
当某个从数据库当选为主数据库后,会通过命令SLAVEOF NO ONE 将自己转换成主数据库,并将旧的主数据库的插槽转换给自己负责。
如果一个至少负责一个插槽的主数据库下线且没有相应的从数据库可以进行故障恢复,则整个集群默认会进入下线状态无法继续工作。如果想在这种情况下使集群仍然能正常工作,可以修改配置cluster-require-full-coverage为no(默认为yes):

cluster-require-full-converage no
文章目录
  1. 1. 重要星级 ★★★★★
  • 集群
    1. 1、配置集群
    2. 2、节点的增加
    3. 3、插槽的分配
    4. 4、获取与插槽对应的节点
    5. 5、故障恢复
  • |