集群是一种分布式的思想,把数据存储到各个节点上去提供服务。分布式一个重要的步骤,就是分片。那redis集群是怎么分片的呢,以及集群服务的稳定性和可靠性怎么保证。下面就来剖析下。
1. 集群是怎么创建的,分片怎么设计的?
分片原理:
redis的核心数据是个hash table,分片是按照key值来划分,再reds里面叫槽(slot),集群中的各个节点都会分一些槽,用于存储它们的数据。
客户端在请求集群的服务时候,通过查询的key,算出一个hash值,再使用hash值%槽数得到第几个槽,再通过对应槽找对应节点,进行查询或者更新操作。
基本数据结构:
集群节点:
// 节点状态 struct clusterNode { // 创建节点的时间 mstime_t ctime; /* Node object creation time. */ // 节点的名字,由 40 个十六进制字符组成 // 例如 68eef66df23420a5862208ef5b1a7005b806f2ff char name[REDIS_CLUSTER_NAMELEN]; /* Node name, hex string, sha1-size */ // 节点标识 // 使用各种不同的标识值记录节点的角色(比如主节点或者从节点), // 以及节点目前所处的状态(比如在线或者下线)。 int flags; /* REDIS_NODE_... */ // 节点当前的配置纪元,用于实现故障转移 uint64_t configEpoch; /* Last configEpoch observed for this node */ // 由这个节点负责处理的槽 // 一共有 REDIS_CLUSTER_SLOTS / 8 个字节长 // 每个字节的每个位记录了一个槽的保存状态 // 位的值为 1 表示槽正由本节点处理,值为 0 则表示槽并非本节点处理 // 比如 slots[0] 的第一个位保存了槽 0 的保存情况 // slots[0] 的第二个位保存了槽 1 的保存情况,以此类推 unsigned char slots[REDIS_CLUSTER_SLOTS/8]; /* slots handled by this node */ // 该节点负责处理的槽数量 int numslots; /* Number of slots handled by this node */ // 如果本节点是主节点,那么用这个属性记录从节点的数量 int numslaves; /* Number of slave nodes, if this is a master */ // 指针数组,指向各个从节点 struct clusterNode **slaves; /* pointers to slave nodes */ // 如果这是一个从节点,那么指向主节点 struct clusterNode *slaveof; /* pointer to the master node */ // 最后一次发送 PING 命令的时间 mstime_t ping_sent; /* Unix time we sent latest ping */ // 最后一次接收 PONG 回复的时间戳 mstime_t pong_received; /* Unix time we received the pong */ // 最后一次被设置为 FAIL 状态的时间 mstime_t fail_time; /* Unix time when FAIL flag was set */ // 最后一次给某个从节点投票的时间 mstime_t voted_time; /* Last time we voted for a slave of this master */ // 最后一次从这个节点接收到复制偏移量的时间 mstime_t repl_offset_time; /* Unix time we received offset for this node */ // 这个节点的复制偏移量 long long repl_offset; /* Last known repl offset for this node. */ // 节点的 IP 地址 char ip[REDIS_IP_STR_LEN]; /* Latest known IP address of this node */ // 节点的端口号 int port; /* Latest known port of this node */ // 保存连接节点所需的有关信息 clusterLink *link; /* TCP/IP link with this node */ // 一个链表,记录了所有其他节点对该节点的下线报告 list *fail_reports; /* List of nodes signaling this as failing */ };这个是集群的原子结构
每个集群节点都有一个对应集群状态的记录,并且server端属性纪录的是集群的状态。
// 集群状态,每个节点都保存着一个这样的状态,记录了它们眼中的集群的样子。 // 另外,虽然这个结构主要用于记录集群的属性,但是为了节约资源, // 有些与节点有关的属性,比如 slots_to_keys 、 failover_auth_count // 也被放到了这个结构里面。 typedef struct clusterState { // 指向当前节点的指针 clusterNode *myself; /* This node */ // 集群当前的配置纪元,用于实现故障转移 uint64_t currentEpoch; // 集群当前的状态:是在线还是下线 int state; /* REDIS_CLUSTER_OK, REDIS_CLUSTER_FAIL, ... */ // 集群中至少处理着一个槽的节点的数量。 int size; /* Num of master nodes with at least one slot */ // 集群节点名单(包括 myself 节点) // 字典的键为节点的名字,字典的值为 clusterNode 结构 dict *nodes; /* Hash table of name -> clusterNode structures */ // 节点黑名单,用于 CLUSTER FORGET 命令 // 防止被 FORGET 的命令重新被添加到集群里面 // (不过现在似乎没有在使用的样子,已废弃?还是尚未实现?) dict *nodes_black_list; /* Nodes we don't re-add for a few seconds. */ // 记录要从当前节点迁移到目标节点的槽,以及迁移的目标节点 // migrating_slots_to[i] = NULL 表示槽 i 未被迁移 // migrating_slots_to[i] = clusterNode_A 表示槽 i 要从本节点迁移至节点 A clusterNode *migrating_slots_to[REDIS_CLUSTER_SLOTS]; // 记录要从源节点迁移到本节点的槽,以及进行迁移的源节点 // importing_slots_from[i] = NULL 表示槽 i 未进行导入 // importing_slots_from[i] = clusterNode_A 表示正从节点 A 中导入槽 i clusterNode *importing_slots_from[REDIS_CLUSTER_SLOTS]; // 负责处理各个槽的节点 // 例如 slots[i] = clusterNode_A 表示槽 i 由节点 A 处理 clusterNode *slots[REDIS_CLUSTER_SLOTS]; } clusterState;创建过程:
redis.c--->int main--->initServer--->clusterInit函数:
// 初始化集群 void clusterInit(void) { int saveconf = 0; // 初始化配置 server.cluster = zmalloc(sizeof(clusterState)); server.cluster->myself = NULL; server.cluster->currentEpoch = 0; server.cluster->state = REDIS_CLUSTER_FAIL; server.cluster->size = 1; server.cluster->todo_before_sleep = 0; server.cluster->nodes = dictCreate(&clusterNodesDictType,NULL); server.cluster->nodes_black_list = dictCreate(&clusterNodesBlackListDictType,NULL); server.cluster->failover_auth_time = 0; server.cluster->failover_auth_count = 0; server.cluster->failover_auth_rank = 0; server.cluster->failover_auth_epoch = 0; server.cluster->lastVoteEpoch = 0; server.cluster->stats_bus_messages_sent = 0; server.cluster->stats_bus_messages_received = 0; memset(server.cluster->slots,0, sizeof(server.cluster->slots)); clusterCloseAllSlots(); //... }2. 集群是如何工作的,但一个命令过来,集群是如何执行命令的?
数据库中的所有的槽,比如说是16384个,都进行了指派,集群就会进入上线状态,这时客户端可以像集群中的节点发送数据命令了。
详细的工作原理:
1)一个命令过来,首先得到此命令要查的key,通过key计算它属于哪个槽。
2)先看看当前节点有没有这个槽,如果有直接查询或操作此节点,并返回消息给客户端。
3)如果此节点没有,那么节点会向客户端返回一个MOVED错误,指引客户端转向至正确的节点,并且再次发送命令,并返回结果。
3. 故障转移
集群中的节点可以设置为主从模式,这样实现了重要数据的备份,单一个节点发生故障的时候,可以把此节点下线,使用它的备份节点(从节点)继续工作,不影响集群的操作。
4. 为什么要用集群
集群的好处有两点,
1)能存储更大的数据量,redis是内存数据库,内存的资源有限,使用集群可以达到大数据量的存储。
2)效率更高,当然这个是对于一定规模的数据来说。数据量小的话,也要分库,中间调度的损耗也挺大的。分库集群,相当于多点处理,能够并发进行,效率当然高多了。
5. 为啥mysql没法实现集群,只能使用分表,这种复杂的处理逻辑?
这个更数据存储结构有关系,redis是hash table结构,而mysql底层结构是B+树,hash表好拆分,B+树不好拆。
mysql分表逻辑就比较复杂了,有垂直拆分和水平拆分,垂直简单点,水平复杂些,但是所有的拆分,都涉及到调用的修改。如果前端直接调用的话,那改动量就大了。