redis 集群模式

redis集群是redis提供的分布式数据库方案,集群通过分片来进行数据共享,并提供复制和故障转移功能。

redis集群的优点

  1. 高可用: 节点故障时,能自动进行故障转移,保证服务的持续可用。
  2. 负载均衡: 工作负载能够被分发到不同的节点上,有效分摊单节点访问压力。
  3. 容灾恢复:通过主从复制、哨兵机制,节点故障时能够快速进行故障恢复
  4. 数据分片: 集群模式下,可以由多个主节点执行写入操作
  5. 易于扩展:可以根据业务需求和系统负载,动态的添加或减少节点,实现水平扩展。

集群的实现原理

一个redis集群通常由多个节点组成。在集群建立前,每个节点都是相互独立的,都处于一个只包含自己的集群中。在组件集群时,需要将每个独立的节点连接起来,构成一个包含多个节点的集群。

节点

一个节点就是一个运行在集群模式下的redis服务器,服务器在启动时回一句cluster-enabled配置来决定是否开启服务器的集群模式。

1
./src/redis-server --cluster-enabled yes # 开启集群模式

运行在集群模式下的服务器,会继续使用所有在单机模式中使用的服务器组件。例如:

  1. 使用文件事件处理器来处理命令请求和返回命令回复。
  2. 使用时间事件处理器执行serverCron函数,在集群模式下继续调用clusterCron函数执行集群模式下的常规操作。
  3. 继续使用数据库保存键值对数据。
  4. 继续使用rdbaof进行持久化。
  5. 继续使用发布订阅机制执行publish、subscribe命令。
  6. 继续使用复制模块进行节点的复制工作。
  7. 使用lua脚本环境来执行客户端输入的lua脚本。

集群创建

在节点A上执行cluster meet ip port,其中ip|port指向节点B,可以让节点A向节点B进行握手,当握手成功时,节点A就会将B节点添加到A节点所在的集群中。

假设目前有A|B|C三个节点,分别对应<ip_a|port_a>、<ip_b|port_b>、<ip_c|port_c>,在未组建集群前,可以理解为三个集群。如果此时需要将三个节点组成一个集群,则需要执行如下命令:

1
2
ip_a:port_a>cluster meet ip_b port_b #将b节点加入到a节点所在集群中
ip_a:port_a>cluster meet ip_c port_c #将c节点加入到a节点所在集群中

在客户端中,可以通过cluster nodes获取当前集群的节点信息。

1
2
3
4
192.167.98.52:6379> cluster nodes
c87ec22247436ee75969a2a524a82cec4d0be9c6 192.167.240.46:6379@16379 master - 0 1693237335871 2 connected 5461-10922
a1520d54c3f1093ba701283cc2a61405776168f2 192.167.98.52:6379@16379 myself,master - 0 1693237334000 3 connected 10923-16383
1e50e943a3ebf1fc955922fff55df6d372861f4b 192.167.76.255:6379@16379 master - 0 1693237334863 1 connected 0-5460

集群数据结构

clusterNode

每一个节点都会使用一个clusterNode结构记录自身状态,并为集群中的其他节点创建一个相应的clusterNode结构,以此记录其他节点的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
typedef struct clusterNode {
mstime_t ctime; // 节点创建时间
char name[CLUSTER_NAMELEN]; // 节点名称
char shard_id[CLUSTER_NAMELEN]; // 分片id
int flags; // 节点标识
uint64_t configEpoch; // 配置纪元
unsigned char slots[CLUSTER_SLOTS/8]; // 当前节点负责哈希槽
uint16_t *slot_info_pairs; /* Slots info represented as (start/end) pair (consecutive index). */
int slot_info_pairs_count; /* Used number of slots in slot_info_pairs */
int numslots; // 当前节点负责处理的槽位数量
int numslaves; // 如果当前节点是主节点,记录其从节点数量
struct clusterNode **slaves; // 指向 从节点 的指针
struct clusterNode *slaveof; // 如果当前节点是从节点,则指向其主节点
unsigned long long last_in_ping_gossip; /* The number of the last carried in the ping gossip section */
mstime_t ping_sent; // 此节点上次发送 ping 消息的时间
mstime_t pong_received; // 上次收到 pong 消息的时间
mstime_t data_received; /* Unix time we received any data */
mstime_t fail_time; // 上次 fail 的时间
mstime_t voted_time; /* Last time we voted for a slave of this master */
mstime_t repl_offset_time; // 上次 接收到 offset 的时间
mstime_t orphaned_time; /* Starting time of orphaned master condition */
long long repl_offset; // 上次接收到的 offset 值
char ip[NET_IP_STR_LEN]; // 节点上次使用的 ip
sds hostname; // hostname
sds human_nodename; /* The known human readable nodename for this node */
int tcp_port; // 客户端tcp端口
int tls_port; // 客户端 tls 使用的端口
int cport; // 节点暴露的端口
clusterLink *link; // 连接节点所需要的信息,比如套接字描述符、输入输出缓冲区
clusterLink *inbound_link; /* TCP/IP link accepted from this node */
list *fail_reports; // 记录其他节点的下线报告
} clusterNode;

clusterNodeFailReport

用于记录与当前节点握手过的其他节点的下线报告。

1
2
3
4
typedef struct clusterNodeFailReport {
struct clusterNode *node; /* Node reporting the failure condition. */
mstime_t time; /* Time of the last report from this node. */
} clusterNodeFailReport;
clusterState

clusterState结构则记录了在当前节点视角下,集群目前所处的状态。如,集群状态、集群节点数量、分片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
typedef struct clusterState {
clusterNode *myself; // 当前节点
uint64_t currentEpoch;
int state; // 集群状态
int size; // 主节点数量
dict *nodes; // 记录 name -> clusterNode的哈希表
dict *shards; // 记录 shard_id -> list(nodes) 的哈希表
dict *nodes_black_list; /* Nodes we don't re-add for a few seconds. */
clusterNode *migrating_slots_to[CLUSTER_SLOTS];
clusterNode *importing_slots_from[CLUSTER_SLOTS];
clusterNode *slots[CLUSTER_SLOTS];
rax *slots_to_channels;
/* The following fields are used to take the slave state on elections. */
mstime_t failover_auth_time; /* Time of previous or next election. */
int failover_auth_count; /* Number of votes received so far. */
int failover_auth_sent; /* True if we already asked for votes. */
int failover_auth_rank; /* This slave rank for current auth request. */
uint64_t failover_auth_epoch; /* Epoch of the current election. */
int cant_failover_reason; /* Why a slave is currently not able to
failover. See the CANT_FAILOVER_* macros. */
/* Manual failover state in common. */
mstime_t mf_end; /* Manual failover time limit (ms unixtime).
It is zero if there is no MF in progress. */
/* Manual failover state of master. */
clusterNode *mf_slave; /* Slave performing the manual failover. */
/* Manual failover state of slave. */
long long mf_master_offset; /* Master offset the slave needs to start MF
or -1 if still not received. */
int mf_can_start; /* If non-zero signal that the manual failover
can start requesting masters vote. */
/* The following fields are used by masters to take state on elections. */
uint64_t lastVoteEpoch; /* Epoch of the last vote granted. */
int todo_before_sleep; /* Things to do in clusterBeforeSleep(). */
/* Stats */
/* Messages received and sent by type. */
long long stats_bus_messages_sent[CLUSTERMSG_TYPE_COUNT];
long long stats_bus_messages_received[CLUSTERMSG_TYPE_COUNT];
long long stats_pfail_nodes; /* Number of nodes in PFAIL status,
excluding nodes without address. */
unsigned long long stat_cluster_links_buffer_limit_exceeded; /* Total number of cluster links freed due to exceeding buffer limit */

/* Bit map for slots that are no longer claimed by the owner in cluster PING
* messages. During slot migration, the owner will stop claiming the slot after
* the ownership transfer. Set the bit corresponding to the slot when a node
* stops claiming the slot. This prevents spreading incorrect information (that
* source still owns the slot) using UPDATE messages. */
unsigned char owner_not_claiming_slot[CLUSTER_SLOTS / 8];
} clusterState;

cluster meet 命令执行过程

通过向节点A发送cluster meet <ip_b> <port_b>,可以将节点B纳入到节点A所在的集群中。
节点A收到命令后,将与节点B进行握手以确定彼此存在,并继续执行如下操作:

  1. 节点A为节点B创建一个clusterNode结构,并将该结构保存至自己的clusterState.nodes字典中。
  2. 节点A依据cluster meet <ip_b> <port_b>命令中的IP地址和端口号向节点B发送一条MEET消息。(后续会介绍消息格式)
  3. 如果一切顺利,节点B接收到节点A发送的meet消息,节点B会为节点A创建一个clusterNode结构,并将此结构添加到自己的clusterState.nodes字典中。
  4. 节点B向节点A发送PONG消息。
  5. 如果节点A收到节点B发送的PONG消息,则节点A可获悉节点B已经收到自己发送的meet消息。
  6. 节点A向节点B返回一条PING命令。
  7. 如果顺利,节点B收到节点A返回的PING消息,以此获悉节点A已经成功接收到节点B返回的PONG消息。此时,握手完成。
    节点A|B集群组建过程的时序图如下:

槽分派

redis集群通过分片的方式来保存数据库中的键值对,集群的整个数据库被分为16384个槽位,数据库中的每个键都属于这些槽中的一个,集群中的每个节点可以处理0个或者16384个槽位。

当数据库中的16384个槽位都有节点在处理时,集群属于上线状态;否则,集群属于下线状态。

集群创建完毕后,可以使用如下命令将槽分配给节点a:

1
ip_a:port_a>cluster addslots <slot> [slot ...]

16384个槽位都有相应节点处理时,集群进入上线状态。可以使用cluster info查看集群状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
192.167.98.52:6379> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:3
cluster_size:3
cluster_current_epoch:3
cluster_my_epoch:3
cluster_stats_messages_ping_sent:849247
cluster_stats_messages_pong_sent:834979
cluster_stats_messages_fail_sent:1
cluster_stats_messages_publish_sent:224
cluster_stats_messages_sent:1684451
cluster_stats_messages_ping_received:834979
cluster_stats_messages_pong_received:849246
cluster_stats_messages_fail_received:1
cluster_stats_messages_publish_received:57273
cluster_stats_messages_received:1741499

相关命令实现原理

cluster addslots

该命令的实现可以理解为对clusterNode.slotsclusterState.slots的更新标注。

执行命令过程

当客户端向节点发送与数据库有关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽位,并检查该槽位是否由当前节点处理。

  1. 如果键所在槽位指派给当前节点,当前节点直接执行命令。
  2. 否则,节点向客户端返回MOVED错误,指引客户端重定向到正确的节点,并再次发送待执行的命令。
1
2
3
192.167.98.52:6379> set mqray181162 hh
-> Redirected to slot [2196] located at 192.167.76.255:6379
OK

通过cluster keyslot可以查阅键所属的槽位:

1
2
192.167.76.255:6379> cluster keyslot mqray181162
(integer) 2196

前面通过cluster nodes已获悉当前节点处理的槽位信息,可以看到

1
2
3
4
192.167.76.255:6379> cluster nodes
a1520d54c3f1093ba701283cc2a61405776168f2 192.167.98.52:6379@16379 master - 0 1693270395063 3 connected 10923-16383
c87ec22247436ee75969a2a524a82cec4d0be9c6 192.167.240.46:6379@16379 master - 0 1693270396071 2 connected 5461-10922
1e50e943a3ebf1fc955922fff55df6d372861f4b 192.167.76.255:6379@16379 myself,master - 0 1693270394000 1 connected 0-5460

可以看到,192.167.98.52:6379节点负责处理的槽位区间为10923-16383,接收到命令时,检测到当前命令操作的键所指向的节点为192.167.76.255:6379后,返回了MOVED错误,重定向后重新执行该命令。
实际上,一个集群客户端通常会维护多个节点的套接字连接,而所谓的重定向只是换一个套接字来发送命令。

key对应槽位的计算方式

使用cluster keyslot key可以获取键所属槽位,其计算公式如下:

1
slot  = CRC16(key) % 16384

节点如何记录槽位

clusterNode.slots

clusterNode中通过如下两个属性记录该节点负责处理的槽位:

1
2
3
4
5
typedef struct clusterNode {
unsigned char slots[CLUSTER_SLOTS/8]; // 当前节点负责哈希槽
uint16_t *slot_info_pairs; /* Slots info represented as (start/end) pair (consecutive index). */
int numslots; // 当前节点负责处理的槽位数量
}

其中slots是一个二进制位数组,数组长度为16384/8=2048字节。
reis以0为起始索引,对slots数组中的16384个二进制位进行编号,根据索引上的二进制位来判断当前节点是否需要处理槽i

对于一个节点而言,检查具体某一个槽位是否被当前节点处理的时间复杂度是O(1)。
numslots即为这个二进制数组中1的个数。

clusterState.slots

一个节点除了将自己所负责的槽位记录在slots数组中,还会将自己的slots数组通过消息的方式发送给集群中的其他节点,以通知其他节点,当前节点负责处理哪些槽位。
集群中的其他节点收到此消息,会更新该节点视角下的clusterState.nodes更新其中传递该消息的节点所负责的槽位分配情况。
因此,集群中的每个节点都知道数据库中16384个槽位的分配情况。

集群槽位分配记录

clusterState结构中记录了如下信息:

1
2
3
typedef struct clusterState {
clusterNode *slots[CLUSTER_SLOTS];
}

slots数组长达16384,记录了每个槽位被分配的节点clusterNode的指针。
如果slots[i]==null,则表示当前槽位尚未分配;如果指向某个clusterNode,则表示当前槽位被分配给该节点。

为什么需要将节点槽位信息存储在 clusterNode中又需要存在 clusterState中?

如果只在clusterNode.slots中记录,则无法高效处理如下情况:

  1. 检测槽位i是否被分配/检测槽位i被分配给了哪个节点: 这两种场景需要遍历集群中的每个节点的clusterNode.slots数组,直到找到该槽位被分配的节点。时间复杂度为O(N)。而如果clusterState.slots存储了,则时间复杂度为O(1)

那么是否可以只将槽位分派信息记录在clusterNode.slots中呢?
由于redis槽位分配中,某节点的槽位分配会通过消息传递给集群中的其他节点,传递消息时只需要将clusterNode.slots传递出去即可。而如果只存在clusterState.slots中,那么每次传播槽位分派信息时,需要遍历clusterState.slots以获取当前的槽位分派信息。

节点数据库的实现

节点和单机服务器在数据方面的区别是: 节点只能使用0号数据库,而单机服务器则没有此限制。
除了将键值对保存在数据库中,还会用clusterState。slots_to_keys记录槽位和键之间的关系。注意,在当前版本中,这一结构被移动到clusterInit.slotToKeyInit函数中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* Initialize slots-keys map of given db. */
void slotToKeyInit(redisDb *db) {
db->slots_to_keys = zcalloc(sizeof(clusterSlotToKeyMapping));
clusterDictMetadata *dictmeta = dictMetadata(db->dict);
dictmeta->db = db;
}

/* Slot to keys mapping for all slots, opaque outside this file. */
struct clusterSlotToKeyMapping {
slotToKeys by_slot[CLUSTER_SLOTS];
};

/* Slot to keys for a single slot. The keys in the same slot are linked together
* using dictEntry metadata. */
typedef struct slotToKeys {
uint64_t count; /* Number of keys in the slot. */
dictEntry *head; /* The first key-value entry in the slot. */
} slotToKeys;

重新分片

redis集群的重新分片是指可以将任意数量的已经指派给某个节点A(源节点)的槽位重新指派给节点B(目标节点),并且相关槽位所属的键值对也会从源节点移动到目标节点。
重新分片操作过程中集群不需要下线,且源节点和目标节点可以继续处理命令请求。

重新分片的实现原理

集群中的重新分配操作是由redis的集群管理软件redis-trib负责执行的。redis提供了进行重新分片所需要的命令,而redis-trib通过向源目标节点发送命令来进行重新分片。
redis-trib对集群的单个槽位进行分片的过程如下:

另外注意到,clusterState结构中记录了集群得重新分片信息。

1
2
3
4
typedef struct clusterState {
clusterNode *migrating_slots_to[CLUSTER_SLOTS];
clusterNode *importing_slots_from[CLUSTER_SLOTS];
}

其中,migrating_slots_to记录了当前节点正在迁移至其他节点的槽。而importing_slots_from则记录了当前节点正在从哪些节点导入槽。

ASK错误

重新分片过程中,可能存在这样一种场景:
待迁移的槽位中键值对部分存在于源节点中,另一部分被存储在目标节点中。
当客户端向源节点发送一个于数据库键有关的命令,并且命令要处理的数据库键恰好属于正在被迁移的槽时:

  1. 源节点会先在自己的数据库中查找键,找到则执行客户端命令;
  2. 如果未找到,则该键有可能已经被迁移到了目标节点,源节点将向客户端返回一个ASK错误,指引这个客户端重定向到正在导入槽的目标节点,并再次发送之前想要执行的命令。(查看clusterState.migrating_slots_to[i]已检查是否在进行迁移,如果指向不为null,则重定向到指针指向的节点)

ASKING

打开发送该命令的客户端的redis_asking标识,以使得客户端在遇到moved错误时能够破例在当前节点中执行关于槽i的命令一次。注意该标识是一次性的,当节点执行了一个带有redis_asking标识的客户端发送的命令后,该标志位将被移除。

ASK错误 和 MOVED 错误

  • MOVED错误: 槽的指派关系发生变化,使得客户端需要从 MOVED错误返回的 ip port 中获取到最新的槽位负责节点然后执行命令
  • ASK错误:是两个节点在迁移槽的过程中的临时措施,当客户端收到关于槽i的ASK错误后,客户端只会在接下来的一次命令请求中将关于槽i的命令请求发送至发生ASK错误所指示的节点。但对该客户端之后的命令请求无效。

复制与故障转移

集群模式中,节点分为主节点和从节点,主节点负责处理槽,从节点负责复制某个主节点,并在被复制的主节点下线时,代替下线主节点继续处理命令请求。

集群中有节点A|b|c,各有三个从节点,此时节点A下线,则将由B|C负责从节点A的从节点中选出一个作为新的主节点,由这个被选中的节点负责处理原先节点A负责的槽位,并继续处理客户端的命令请求。

配置从节点

在前文中,集群架构还只是顶层的多个主节点。集群模式当然是支持为节点分配从节点的。使用如下命令可以让接收命令的节点成为node_id所指定的从节点,并开始对主节点进行复制。

1
cluster replicate <node_id >

从节点如何复制主节点

  1. 收到命令的节点从clusterNode.nodes中找到noed_id对应节点的clusterNode结构,并且将自己的clusterState.myself.slaveof指向该节点,以记录当前正在复制的主节点。另外,将clusterState.myself.flags修改为REDIS_NODE_slave,以标识当前是从节点。
  2. 根据clusterState.myself.slaveof指向的clusterNode中保存的IP|port对主节点进行复制。
    slaveof ip pport
  3. 一个节点成为从节点,并且开始复制主节点这一信息会通过消息发送给集群中的其他节点
    ,最终所有节点都会知道这一消息。

故障检测

疑似下线 pfail

集群中的每个节点都会定期向集群中的其他节点发送ping消息,以此检测对方是否在线,如果该命令没有在规定时间内响应,那么发送消息的节点会将收到消息的节点标记为pfail, 疑似下线
同理,集群中的各个节点会通过互相发消息的方式来交换集群中各个节点的状态信息。
如果主节点A通过消息得知主节点B认为主节点C进入疑似下线状态,则主节点a会将clusterNode.nodes中找到节点C,并将节点B报告的下线报告加入到clusterNode.fail_reports链表中。
每个下线报告中记录了如下信息:

1
2
3
4
5
/* This structure represent elements of node->fail_reports. */
typedef struct clusterNodeFailReport {
struct clusterNode *node; // 报告目标节点下线的主节点
mstime_t time; // 最后一次从node收到下线报告的时间
} clusterNodeFailReport;
已下线 fail

如果在一个集群中,半数以上负责处理槽的主节点都将某个主节点X报告为疑似下线,那么这个节点X将被标记为已下线,将主节点标记为已下线的节点会向集群广播一条关于主节点Xfail消息,收到此消息的所有节点会立即将clusterNode.nodes中节点X的状态修改为已下线。

故障转移

如果从节点检测到自己正在复制的主节点进入了下线状态,从节点将开始对下线主节点进行故障转移:

  1. 复制下线节点的所有从节点中选出一个。
  2. 被选中的从节点执行slaveof no one,成为主节点
  3. 原先指派给下线主节点的槽位重新指派给当前节点。
  4. 向集群广播一条pong消息,通知集群中的其他节点,当前节点已成为主节点,并且负责接管下线节点所负责的槽位。
  5. 新的主节点开始接收和自己负责槽位相关的命令请求,故障转移完成。
集群选主

从节点检测到主节点下线后如何进行选主?

  1. 集群中的某个节点开始一次故障转移操作时,集群的配置纪元+1.
  2. 在每个配置纪元中,集群中每个负责处理槽的主节点拥有一次投票机会,第一个向此主节点请求投票的节点将获得投票。
  3. 从节点发现复制的主节点下线时,将广播CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到消息且具有投票权的主节点进行投票。
  4. 具有投票权且尚未投票的主节点,将返回消息CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK
  5. 每个从节点统计自己获取的投票数量,如果得票数大于等于N/2+1,则此从节点将当选成为新的主节点。N是集群中具有投票权的主节点数量。
  6. 如果一个配置纪元中无法选出主节点,则将进行下一次选举。

消息

节点发送的消息主要有如下五种:

  1. MEET: 发送者通过客户端向接受者发送,标识请
  2. PING: 集群中的每个节点默认每隔一秒从已知的节点列表中随机选出5个节点,对最长时间没有发送过ping消息的节点发送ping消息,以此检测被选中的节点是否在线。如果节点a最后一次收到节点b发送的PONG消息的时间距当前时间已超出节点a设置的cluster-node-timeout的一般,那么节点a也会向节点B发送ping消息。
  3. PONG:接收者回复meet|ping命令;或者向节点广播PONG命令以通知其他节点当前节点状态变化。
  4. FAIL:主节点A判断主节点B进入Fail状态,节点A会向集群广播一条关于节点Bfail的消息,所有收到此消息的节点都会立即将节点B标记为已下线。
  5. publish: 节点收到publish命令时,节点会执行命令,并向集群广播一条publish消息,所有接收到此消息的节点都会执行相同的publish命令。

gossip协议

扩容 缩容

扩容方案

  1. 将新节点纳入集群, 使用cluster meet或者 redis-trib add node
  2. 确定 加入的新节点 所负责的槽位, 同时查询clusterState.slots查询该槽位的原先被指派的主节点。
  3. 遍历 所有槽位, 将每个槽位关联的节点中的 键值对 迁移到 新加入的节点中。
  4. 该主节点负责的槽位全部迁移完毕,向集群广播当前的节点状态,负责迁移后槽位,以及相关命令的执行。

缩容方案

  1. 是否是主节点。
  2. 是主节点,有无被分派的槽位。
  3. 有分配的槽位,先将当前负责的槽位分配到其他节点。
  4. 所有槽位完成重新指派并完成数据库键值对的迁移后,广播当前节点准备下线。
  5. 原先该主节点的从节点下线。

思考?

为什么redis支持16384个槽位?

前面我们提到过,redis中计算一个键所对应的槽位的计算方式是:

1
slot  = CRC16(key) % 16384

而CRC16能够获得65535个值,那为什么redis只支持16384个呢?
原因在于,redis的各种检测命令、广播命令中,都会将携带slots信息。两者的开销分别是 2^16/8=8KB2^14/8=2KB
而redis集群模式最多支持1000个分片,故而选择16384相对65535是更合理的选择。

为什么要传全量的slot状态?

因为分布式场景,基于状态的设计更合理,状态的传播具有幂等性

为什么不考虑压缩?

集群规模较小的场景下,每个分片负责大量的slot,很难压缩。

详见https://github.com/redis/redis/issues/2576

为什么 集群模式中不适用 发布订阅

所有的publish命令都会向所有节点(包括从节点)进行广播,造成每条publish数据都会在集群内所有节点传播一次,加重了带宽负担,对于在有大量节点的集群中频繁使用pub,会严重消耗带宽。

引用

1. redis集群模式
2. 状态检测及维护