一、Redis集群特点

1.1 Redis集群拓扑结构

Redis集群是一个网状结构,每个节点都通过TCP连接跟其它每个节点连接。

在一个有N个节点的集群中,每个节点都有N-1个流出的TCP连接和N-1个流入的TCP连接,这些TCP连接会永久保持,并不是按需创建的。

1.2 Redis集群数据分片

Redis集群的实现方案:

  • 客户端分区,代表为 Redis Sharding

  • 代理分区方案,主流实现的有方案有 Twemproxy 和 Codis

  • 查询路由方案,Redis默认实现方案。

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

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

节点 A 包含 0 到 5500号哈希槽.
节点 B 包含5501 到 11000 号哈希槽.
节点 C 包含11001 到 16384号哈希槽.
这种结构很容易添加或者删除节点. 比如如果我想新添加个节点D, 我需要从节点 A, B, C中得部分槽到D上. 如果我想移除节点A,需要将A中的槽移到B和C节点上,然后将没有任何槽的A节点从集群中移除即可. 由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态。

二、为什么Redis的集群使用16384个哈希槽?

这个问题Redis的作者又在github的issue中解释过,我们看一下原文:原文地址

why redis-cluster use 16384 slots? crc16() can have 2^16 -1=65535 different remainders。

The reason is:

  1. Normal heartbeat packets carry the full configuration of a node, that can be replaced in an idempotent way with the old in order to update an old config. This means they contain the slots configuration for a node, in raw form, that uses 2k of space with16k slots, but would use a prohibitive 8k of space using 65k slots.
  2. At the same time it is unlikely that Redis Cluster would scale to more than 1000 mater nodes because of other design tradeoffs.
    So 16k was in the right range to ensure enough slots per master with a max of 1000 maters, but a small enough number to propagate the slot configuration as a raw bitmap easily. Note that in small clusters the bitmap would be hard to compress because when N is small the bitmap would have slots/N bits set that is a large percentage of bits set.

三、总结

从上面Redis的集群拓扑架构中我们知道,Redis集群节点是通过gossip协议进行点对点通信的,每个节点都会与集群中的其它节点进行通信,我们先看一下Redis心跳包的数据结构。

typedef struct {
    char sig[4];        /* Signature "RCmb" (Redis Cluster message bus). */
    uint32_t totlen;    /* Total length of this message */
    uint16_t ver;       /* Protocol version, currently set to 1. */
    uint16_t port;      /* TCP base port number. */
    uint16_t type;      /* Message type */
    uint16_t count;     /* Only used for some kind of messages. */
    uint64_t currentEpoch;  /* The epoch accordingly to the sending node. */
    uint64_t configEpoch;   /* The config epoch if it's a master, or the last
                               epoch advertised by its master if it is a
                               slave. */
    uint64_t offset;    /* Master replication offset if node is a master or
                           processed replication offset if node is a slave. */
    char sender[CLUSTER_NAMELEN]; /* Name of the sender node */
    unsigned char myslots[CLUSTER_SLOTS/8];
    char slaveof[CLUSTER_NAMELEN];
    char myip[NET_IP_STR_LEN];    /* Sender IP, if not all zeroed. */
    char notused1[34];  /* 34 bytes reserved for future usage. */
    uint16_t cport;      /* Sender TCP cluster bus port */
    uint16_t flags;      /* Sender node flags */
    unsigned char state; /* Cluster state from the POV of the sender */
    unsigned char mflags[3]; /* Message flags: CLUSTERMSG_FLAG[012]_... */
    union clusterMsgData data;
} clusterMsg;

消息头里面有个myslots的char数组,长度为16383/8,这其实是一个bitmap,每一个位代表一个槽,如果该位为1,表示这个槽是属于这个节点的。

在Redis的心跳包携带了节点的全部配置信息,我们可以方便的去更新一个旧节点的全部信息。在心跳包中包含了为这个节点配置的全部哈希槽信息,他是一个位图数据结构。
(1)如果使用16384个哈希槽,则这个字段大小为2KB,如果使用65535个哈希槽,则将占用8KB的空间。
而且由于其它设计上的权衡,一个Redis集群的规模不可能超过1000个节点。
(2)因此16384个哈希槽能确保一个Redis集群上的每个主节点分配到足够的哈希槽;而且16384个哈希槽足够小,可以将节点配置的哈希槽作为原始位图进行传输。
而且由于在Redis集群规模较小时,由于每个节点分配的哈希槽会比较多,位图很难被压缩。