Redis 集群


主从复制指将主 Radis 的数据复制到其他从属 Radis 中,数据的复制只能从主机复制到从机,并且从机不可以写入数据

主从复制的好处是读写分离已经容灾后快速恢复,主机读数据,从机写数据,因为大多数对数据库的操作都是读取数据,这么做可以减少服务器压力

基础与特性

首先要明确这台 reids 是否支持集群化,查看配置的 cluster-enabled 属性,如果该属性为 yes,则表示支持集群

redis 使用 cluster meet IP 端口命令来连接另外一台支持集群的 redis 机器,使用 cluster node 命令查看该节点的集群状态,如果这个节点只有它自己,也是可以被称为集群的

集群的特性有很多,比如从机下可以挂从机,薪火相传就是指从机下可以挂从机这一特性,缺点是某个中间节点的从机挂掉后后续的从机不能读取数据

使用命令 slaveof no one 来实现主从机的切换,当主机挂了之后这个从机自动成为主机,主机一般被称作 master,从机一般被称作 slave

我们可以得出,如何复制数据以及主机挂掉之后从机如何成为主机这两个问题比较主要。基于这个问题,我们来介绍一下 redis 的三种模式:主从模式、哨兵模式、cluster

心跳检测

在互联网多个机器沟通的时候,心跳检测十分重要,毕竟是用来确认对方是否活着的重要依据,在几乎所有的集群搭建中,底层都实现了心跳检测机制。redis 的心跳检测主要做两件事:

1,检测主从服务器的网络状态:主机每一秒都会发送检测请求,并且将该值保存,如果发现 lag 的值超过1秒的话,主从连接大概率出现问题了

2,检测命令丢失:即使对方存活的情况下也无法保证对方是否接受到了命令,命令可能由于网络等等情况丢失。因此 redis 的心跳检测还会携带从机的复制偏移量,如果主机发现返回的复制偏移量小于当前的复制偏移量,主机就知道从机应该是丢失数据了,此时会采取一定的补救措施

数据的主从复制

复制的旧版实现

需要复制的时候向被认定为从机的 redis 机器输入 slaveof 指令即可

slaveof 127.0.0.1 6379

主从复制的原理是从机连接主机时会发送一个 sync(同步)命令,主机接受后会发送存放在主机中的所有数据给从机(全量复制),在执行修改后也会发生数据给从机(增量复制)

全量复制是执行 bgsave 命令,使用子线程生成一个当前数据的 RDB 文件,并且将这个文件发送给从机。但是生成并且发送数据的时候也有可能在主机中生成数据,因此主机会用一个缓冲区记录所有的写数据。在从机载入 RDB 文件后还会读取主机缓冲区中的命令,此时主从复制完成

在主机写数据时候需要向从机发送写数据的指令,保证主从一致,这种行为叫命令传播,简单来说就是先全量后增量的变更数据

但是旧版全量复制的时候有个缺点,就是在主从机断链的并且恢复的时候,主机会重新向从机发送 RDB 文件,这个过程是非常消耗资源的。因此 redis 将该动作升级了一下

复制的新版实现

为了处理重新链接时消耗过多的问题,我们可能想到让从机从断裂处开始读取数据

新版使用 psync 命令来代替 sync 命令,其中关于部分重同步的底层实现更变成了在从机断掉之后会选择性的进行 RDB 复制或者是只将部分数据复制

在主机向从机发送增量更新命令时主从服务器都会维护一个复制偏移量,该值记录了主机已经向从机发送了多少字节,主机每次向从机发送 N 字节数据在主服务器中该值就增加 N 字节。从服务器中的复制偏移量记录了当前最新数据的偏移量。这样发生网络中断时该值不会更新,我们就知道从机是从什么时候开始中断的

复制积压缓冲区是一个固定长度的先进先出队列,主服务器用它来记录之前发送的语句偏移量与字符对应关系,如果从机重新链接后给出的复制偏移量小于缓冲区的最小值,就执行 RDB 全量复制,如果大于改值说明缓冲区中存放了复制需要的语句。因此可以直接复制

主从复制时数据丢失问题

1,异步复制导致的数据丢失:因为数据从主机复制到从机需要一定时间,当数据没有完全复制到从机时,主机就挂了,此时数据会丢失

在主机开启持久化之后数据依然会丢失,因为集群检测到 master 发生故障,会重新选举新的 master。新的 master 没有收到这份数据,那么旧 master 重新上线后里面的数据就会被刷新掉,此时数据还是会丢失

2,脑裂导致的数据丢失:假设我们有一个 redis 集群,正常情况下 client 会向 master 发送请求,然后同步到 salve,sentinel 集群监控着集群,在集群发生故障时进行自动故障转移。此时,由于某种原因,比如网络原因,集群出现了分区,master 与 slave 节点之间断开了联系,sentinel 监控到一段时间没有联系认为 master 故障,然后重新选举,将 slave 切换为新的 master。但是 master 可能并没有发生故障,只是网络产生分区,此时 client 任然在旧的 master 上写数据,导致数据丢失

脑裂:由于某些原因导致集群被分成多个部分,原来的 master 正常提供服务,但是哨兵认为它挂了,选出一个新的 master,导致同一时间有多个 master 向外提供服务

发生了这两种情况主机都是可以探测到的,思路就是探测到之后进行一些补救措施,比如主机禁止写入

min-slaves-to-write 1
min-slaves-max-lag 10

min-slaves-to-write 默认情况下是0,min-slaves-max-lag 默认情况下是10。以上面配置为例,这两个参数表示至少有1个 salve 的与 master 的同步复制延迟不能超过10s,一旦所有的 slave 复制和同步的延迟达到了10s(没有确认到从机接受到数据了),那么此时 master 就不会接受任何请求。redis 采用异步复制的操作,因此不能配置为等到所有的从机都接受到数据后,才给客户端发回响应

关于脑裂也是如此,如果当前主机控制的从机少于一定数量时(min-slaves-max-lag = 10代表少于10时),不会写入数据

哨兵 sentinel

从机挂了主机可以检查到并且自动处理问题,那主机挂了服务就真挂了,如何保证服务的高可用性呢

我们可以配置一主二从三哨兵来处理这个问题,哨兵也是一个 redis 服务,哨兵是 redis 高可用的解决方案。而为了哨兵的高可用一般需要配置哨兵集群,哨兵的功能是监视所有的 radis 集群,在主机挂了之后,自动配置第二个主机,如果故障转移发生了,他还要通知 client 客户端新的 master 地址

哨兵集群也会有相互发送心跳检测的功能。哨兵与普通 redis 的连接有两个,一个是命令连接(用于发送命令),一个是订阅连接(发送订阅模式中,主机在发送数据后本地不保存数据,万一数据丢失则无法再次发送,因此在哨兵里解决了这一问题)。哨兵与哨兵之间的连接只有命令连接

成为哨兵意味着失去大多数的命令解析能力,服务器在命令表中没有载入 set、del 等普通命令,下面是 redis 5.0.9 哨兵可以执行的所有命令

struct redisCommand sentinelcmds[] = {
    {"ping",pingCommand,1,"",0,NULL,0,0,0,0,0},
    {"sentinel",sentinelCommand,-2,"",0,NULL,0,0,0,0,0},
    {"subscribe",subscribeCommand,-2,"",0,NULL,0,0,0,0,0},
    {"unsubscribe",unsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
    {"psubscribe",psubscribeCommand,-2,"",0,NULL,0,0,0,0,0},
    {"punsubscribe",punsubscribeCommand,-1,"",0,NULL,0,0,0,0,0},
    {"publish",sentinelPublishCommand,3,"",0,NULL,0,0,0,0,0},
    {"info",sentinelInfoCommand,-1,"",0,NULL,0,0,0,0,0},
    {"role",sentinelRoleCommand,1,"l",0,NULL,0,0,0,0,0},
    {"client",clientCommand,-2,"rs",0,NULL,0,0,0,0,0},
    {"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0},
    {"auth",authCommand,2,"sltF",0,NULL,0,0,0,0,0}
};

哨兵是按照如下过程检测 radis 主机是否挂了的:

  • 主观下线:哨兵给主机发信息,如果不回信息,哨兵会主观认为主机挂了
  • 客观下线:很多哨兵(超过一半的哨兵)都发现主机挂了,进行投票之后换了一个主机,称为原来主机的客观下线

哨兵选举

在大多数哨兵认定主机客观下线之后,就需要进行性的哨兵选举了。哨兵的选举采用的是 Raft 算法,Raft 是一个用户管理日志一致性的协议

大致按照如下顺序:

  • 每个做主观下线的 sentinel 节点向其他 sentinel 节点发送命令,要求将自己设置为领导者
  • 接收到的 sentinel 可以同意或者拒绝
  • 如果该 sentinel 节点发现自己的票数已经超过半数并且超过了 quorum
  • 如果此过程选举出了多个领导者,那么将等待一段时重新进行选举。选出 Leader 后 Leader 会从从机中选举出合适的丛机进行故障转移

重点强调一下 raft,他其实重点侧重于在一群机器中选择出一个老大,而且他最初的目的是管理日志一致性,即这个老大原本应该会受到请求,并且把请求作为日志条目加入到它的日志中,然后并行的向其他服务器发起请求以复制日志条目。当这条日志被复制到大多数服务器上,Leader 将这条日志应用到它的状态机并向客户端返回执行结果。redis 只借鉴了它前半部分,舍弃了后半部分
在这里插入图片描述

新的主机选出来之后,我们还要干两件事,让已下线的主机下的所有从机复制新的主机的内容,保证数据一致性;将旧的主服务器挂到这个新主机下,变成新主机的从机,这样在下次上线的时候就不用配置了。这些都可以通过固定命令完成

高可用架构 RedisCluster

redis 使用集群分摊压力、实现扩容,获得更多的内存空间,redis 实现了自己的集群连接方式保证了一定的高可用性

之前我们会使用主从复制加哨兵模式来保证服务的高可用,配合上 redis 的其他特性(薪火相传等),这个集群会运行的很好,其中的原理在上面都已经讲过了,但是,都什么年代了还在用传统集群

去中心化集群架构

redis 可以采用无中心化集群(每个 redis 都有从机,与其他 redis 可以相互通信,每个 redis 都可以被访问),而数据的分配一般使用数据分片槽指派)来实现

所谓数据分片,就是一共有16384个 slot(槽),数据库中的每个键都能通过算法(使用公式 CRC16(key) % 16384来计算)算出需要操作的数据属于这16384个哈希槽的哪一个

如果16384个槽没有被占满,即集群中的 redis 服务器们没有负责全部的槽,此时的集群服务被称作下线状态,相反,如果 redis 占满了所有的槽,此时服务处于上线状态

而集群中的每个节点负责处理一部分哈希槽,这些节点又叫 redis 分片,比如:

  • 节点 A 负责处理0号至8000号哈希槽
  • 节点 B 负责处理8001号至16384号哈希槽

而如果想要添加或者删除节点的话,将其他节点的槽放入或者增加就行了,槽中的数据也会一并转移

我们既然要保证高可用,就要考虑到这过程中的各种问题,假如突然有一台机器宕机了,如何将该机器的槽以及数据快速迁移到其他机器上,并且保证该过程中用户的访问是正常的呢?

集群的底层数据结构

redis 集群使用三个数据结构来维持

redis 集群由多个节点组成,为了让某个机器了解集群中其他机器的状态,必须在服务器内部维持一个表示其他机器的数据结构,即 clusterNode

typedef struct clusterNode {
    mstime_t ctime; /* 创建节点的时间 */
    char name[CLUSTER_NAMELEN]; /* Node name, hex string, sha1-size */
    int flags;      /* 节点标识 */
    uint64_t configEpoch; /* Last configEpoch observed for this node */
    unsigned char slots[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; /* 从节点 */
    struct clusterNode *slaveof; /* 父节点 */
    ......
}

clusterNode 的 link 属性是一个 clusterLink 结构,表示与连接节点相关的信息,clusterLink 结构既然是表示连接的,那与 redisClient 类似,也有输入缓冲区与输出缓冲区,表示了向其他节点发送消息的相关属性

typedef struct clusterLink {
    mstime_t ctime;             /* 连接开始时间 */
    int fd;                     /* 代表了套接字 */
    sds sndbuf;                 /* 输出缓冲区 */
    sds rcvbuf;                 /* 输入缓冲区 */
    struct clusterNode *node;   /* 与这个连接相关联的节点 */
} clusterLink;

我们使用 clusterState 结构来表示在当前节点的视角下,整个集群是什么样子的,上面两个数据结构都是一对一的关系,这个结构则代表比较整体的概念

typedef struct clusterState {
    clusterNode *myself;  /* 自己 */
    uint64_t currentEpoch;
    int state;            /* 集群当前状态,可能是 CLUSTER_OK, CLUSTER_FAIL, ... */
    int size;             /* 至少处理一个槽的节点的数目 */
    dict *nodes;          /* 名称为键,clusterNode 为值的字典 -> clusterNode structures */
    dict *nodes_black_list; /* Nodes we don't re-add for a few seconds. */
    ......
}

来看看它们的具体应用:

cluster meet 命令借鉴(抄袭)了 TCP 的三次握手,该命令的执行就是两台 redis 的握手过程,首先发送方创建一个 clusterNode 结构,将该结构添加到 clusterState.nodes 字典中,然后发送一条 MEET 消息,那一台 redis 接受后也会执行相同的操作(创建结构,添加字典,发送 MEET),A 接受会会返回一条 PONG 消息,握手结束

之后,B 会向集群中通过 gossip 协议向集群中其他节点发送 A(发送方)的 MEET 请求,最终,集群中所有的机器都会认识 A

如何快速找到槽中数据(前缀树)

除此之外节点还会在接受到槽更新命令的时候在集群中还会发送节点的槽指派信息,其他的机器接受后,会改变clusterNode 以及 clusterState 中的 slots相关信息

也就是说,槽是由 clusterNode 以及 clusterState 中的槽数组配合储存的,在每个 node 节点中储存的是比特数组,使用1来表示该槽由该机器负责。该机器对应的 slots 只会记录该机器负责的槽,其他机器负责的槽记录为0

    unsigned char slots[CLUSTER_SLOTS/8]; /* slots handled by this node */

但是这么做会导致很多操作会消耗巨大的时间复杂度,比如查询某个槽应该由哪个节点负责,我们就不得不遍历所有节点的所有槽。因此我们还需要使用 clusterState 一起记录信息

    clusterNode *slots[CLUSTER_SLOTS];

clusterState 里的 slots 记录了集群中节点的指针,所有槽都可以通过指针很快的找到对应关系,而没有节点负责的槽会指向 null

了解了槽的分配方式,我们下一步应该去了解节点是如何判断数据属于哪个槽的,我们知道 redis 是储存键值对的数据库。因此由相应的 hash 函数将数据转换为数字,我们可以用数字来判断槽的位置。redis 中使用以下的 hash 算法

def slot_number(key):
	return CRC16(key) & 16383

16384的二进制为100000000000000,16383则为多个连续的1,将利用 CRC16 算法算出来的数据进行按位与运算,可以保证数据在槽数组中一定有相应位置。因此节点判断某个数据是否在自己负责的槽中,只需要根据算出来的结果与 clusterState 进行比较即可

如果该位置是其他节点的 clusterNode 指针,则根据相应的 clusterNode 内容拿到对应的 IP 以及端口,向用户返回 MOVED 即可,随后用户的客户端会自动根据返回的结果访问相应的节点

除了以上两个数组用来记录管理 redis 的数据储存于集群节点的关系之外,还有一个跳表储存了数据与节点的相关信息。redis 的这种架构将多个数据打散,尽可能均匀的平分到每个节点之中。我们可以实现通过一个数据的键快速的找到对应的槽的以及节点,那如何通过一个槽来获取存放在槽中的键呢

zskiplist *slots_to_keys;

redisState 中的跳表可以根据分值快速的找到数据的区间范围,非常适合处理这种情形,我们将每个分值代表一个槽号,每个键代表一个数据库键。但是这种技术选型真的是最优解吗?

在我下载的版本5.0.9中,跳表已经被替换成了 rax

    rax *slots_to_keys;
    
typedef struct raxNode {
    uint32_t iskey:1;     /* Does this node contain a key? */
    uint32_t isnull:1;    /* Associated value is NULL (don't store it). */
    uint32_t iscompr:1;   /* Node is compressed. */
    uint32_t size:29;     /* Number of children, or compressed string len. */
    unsigned char data[];
} raxNode;

typedef struct rax {
    raxNode *head;
    uint64_t numele;
    uint64_t numnodes;
} rax;

rax 这种数据结构是前缀树。简单来说,前缀树的根节点不保存任何字符,而除了根节点以外的其他节点,每个节点只保存一个字符。当把从根节点到当前节点的路径上的字符拼接在一起时,就可以得到相应 key 的值了

              (f) ""
                \
                (o) "f"
                  \
                  (o) "fo"
                  /  \
               (t)    (b) "foo"
                /      \
         "foot" (e)     (a) "foob"
              /           \
      "foote" (r)         (r) "fooba"
            /               \
  "footer" []               [] "foobar"

这种数据结构的好处是可以充分利用空间,记录尽可能多的数据,对于集群这种需要处理巨量数据的需求非常好用

之前的版本使用 skipList 结构存储,优化之后使用 rax 压缩前缀法,节省了更多的空间,和 skiplist 的时间复杂度一样但是空间复杂度降低,因为 rax 只存储 key 值,以及下一个节点,不存储其他信息。但是 rax 给 key 值增加一个前缀,前缀就是槽的位置。比如,set name lzg,name 在槽点10086上面,rax 上面存储的就是 1086name

重新分片

我们先来看看普通的重新分片操作,在某个机器运行良好的情况下将这台机器上的一部分槽分配给另外一条机器(如果机器挂了,还是老老实实让从机上线吧)。redis 通过 redis-trib 这个集群管理软件负责执行的,redis-trib.rb 是官方提供的 Redis Cluster 的管理工具,无需额外下载,默认位于源码包的 src 目录下,但因该工具是用 ruby 开发的,所以需要准备相关的依赖环境(trib 是三边形的意思)

机器收到指令后会指行以下的操作:

1,redis-trid 会向目标节点发送 cluster setslot 槽 importing 源节点 IP 这个命令,让目标准备接受数据,然后向源节点发送 cluster setslot 槽 migrating 目标 IP 命令让源节点准备发送数据(migrate 迁移)
2,redis-trid 发送命令获取最多 x 个键,然后向源节点发送命令,原子的将数据迁移到目标节点中。重复这一过程,直到所有的数据迁移完毕
3,redis-trid 向其他节点发送消息,让其他节点中的数据中的槽指派给目标节点

cluster setslot importing 命令会让目标节点在 clusterState 中的 importing_slots_from 发生变化。如果该属性的槽指向不为 null,而是一个 clusterNode,代表现在该节点的这些槽正在接受目标节点的数据迁移

    clusterNode *importing_slots_from[CLUSTER_SLOTS];

cluster setslot migrating 命令会让源节点的 migrating_slots_to 属性发生变化,如果该属性的槽指向不为 null,而是一个 clusterNode,代表现在该节点的这些槽正在向目标节点迁移数据

    clusterNode *migrating_slots_to[CLUSTER_SLOTS];

在迁移的过程中如果用户访问正在迁移的键怎么办呢?由于数据要么在源节点要么在目标节点,我们可以根据这个特性来解决问题

由于此时其他节点的槽还并没有改变,因此请求会打到源节点,在迁移数据时维持高可用的做法就是,节点会判断 key 在不在自己的数据库中,如果没找到,去用该 key 对应的槽查找 migrating_slots_to 数组中对应的下标,如果该下标不为 null,会发送 ASK 错误给用户,而 ASK 中带有目标节点的 IP 与端口。如果下标为 null,那就是没有该数据了

ASK、MOVED、ASKING

MOVED

我们知道如果某个槽不由 A 节点处理,A 节点会返回给客户端一个 MOVED 命令,该命令指向了负责该区域的节点,但是在槽重新分配的时候,我们可能遇到更多的奇怪情况,因此 redis 做了一些特殊处理

比如 redis 是先转移数据再转移槽的,假如这时候 A 节点向 B 节点转移数据,槽还在 A,数据已经在 B 了。那么会发生如下情况:

客户端访问 A 以使用某个键的值,redis 会先检测该数据的槽是否由 A 负责,如果是,并且在 A 中没有找到这个数据的时候,A 会检测自己 clusterState.migrating_slots_to[i] 中的值,看对应的槽是否在进行迁移,如果是,节点会发送一个 ASK 命令指引客户端去访问正确的节点

为什么不发送 MOVED 命令呢,因为它们有不同的使用范围,MOVED 是数据分片完成之后的类似永久重定向的命令,而 ASK 只是一种暂时的措施,只在数据迁移时临时发挥作用保证数据的正确性

ASK 与 MOVED 还有一个区别,ASK 在访问正在导入槽的节点的时候会向这个节点先发送 ASKING 命令,再发送语句执行命令。ASKING 命令唯一的用处就是让该集群去检查自己的 clusterState.importing_slots_from[i],如果该节点正在导入槽 i,则破格执行该命令。因为此时,这个节点还没有负责槽 i,只是在导入数据而已,如果直接发送命令,则会返回 MOVED 命令

复制与故障转移

在做高可用系统的时候,不可能只有一个机子负责一部分的槽,如果该机器挂了,我们应该使用一定的错误检测手段,以及发现故障之后的维护高可用的故障转移手段

联系之前学习过的哨兵集群,我们可能想到所有的节点都配置上子节点,这样在主节点崩溃之后就可以快速替换了。而我们还可以使用哨兵来实现故障的检测与自动转移,但是,我们都用心跳检测来保证集群的检测了,还有必要配置哨兵浪费资源吗

redis 是这么做来进行检测与自动转移的:集群中每个节点都会定期的向集群中的其他节点发送 PING 消息,也就是心跳检测。如果在一定时间范围内,没有收到对方的回应,则该节点认定对方为疑似下线状态

集群中的状态可能是在线状态,疑似下线状态以及已下线状态。当其他节点收到某个节点将另外一个节点标记为疑似下线状态,还会在自己的某个字典中添加疑似下线节点的下线报告

如果集群中半数以上的节点都认为某个节点疑似下线,则会进入故障转移阶段。大体过程为,选择一个从节点成为主节点,获取获取所有的数据以及槽,改变集群中的数据结构

而从节点就用来复制主节点内容,以及在检测到主节点崩溃之后自己上线为主节点,用来的主节点重新上线后,会变成从节点。这里面的要点有这么几个

  • 一个主节点可能有很多从节点,如何确定是哪一个从节点应该替换主节点呢?
  • 在从节点复制主节点时,会发生什么,换句话说,从节点的相关信息在哪些节点的哪些数据结构中保存呢?

确定是哪一个从节点应该替换主节点跟配置纪元有关。当一个从节点发现自己复制的主节点处于已下线状态(接受到集群中的关于主节点的 FAIL 消息),就会开始进行投票选举,过程很简单:

集群的配置纪元(epoch,这里特指 current epoch,因为 redis 中还有一个纪元)是一个自增计数器,表示了选举的版本号。每进行一轮投票,该计数器加1,纪元的值会在投票消息中发给各个节点,这样解决了时间层次上的冲突。而在纪元修改完毕之后,该挂了的主节点下所有的从节点都会向集群中的每个主节点广播一条 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 消息以要求主节点投票。每个主节点只会投给第一个到达的从节点投票,当一个从节点收到了大于等于集群中一半机器的投票,该从节点会成为主节点,如果所有的从节点都没有收到一半的票数,则纪元加一重新开始投票

投票的过程和 Sentinel 方法类似,它们都参考了 Raft 算法的领头选举方法来实现。投票保证了该从节点与每个主节点都能联通,而只投给第一个其实保证了该从节点与其他主节点的联系速度最快

gossip 协议

Redis 分片之间也通过 Gossip 协议进行通讯,Gossip 协议百度翻译成中文是八卦,简单点说是一种弱最终一致性算法,主要用于解决大规模去中心化 P2P 网络中的数据一致性问题

算法的灵感来自著名的六度分隔理论,即你和任何一个陌生人之间所间隔的人不会超过六个,也就是说,最多通过六个人你就能够认识任何一个陌生人

因此,节点只需要向自己认识的节点发送消息就行了,gossip 协议的目标是把一些发生的事件传播到整个集群之中

该协议发送数据只干两件事

1,随机选择 N 个尚未发送过消息的邻接节点发送消息
2,收到信息的节点等待一段时间,再重复上述步骤

消息

集群中的每个节点通过发送消息与接受消息来通信。你可能觉得这句话是废话,但是它非常重要,它规定了节点之间通信必须遵守的原则,这也是 redis 不能向集群中广播 publish 消息的原因

节点之间互相发送的消息大体分为以下5种:

1,MEET 消息,该消息用于握手。发送者会向接收者发送 MEET 消息让接收者将自己拉进集群中

2,PING 消息,用于心跳检测。有以下两种情况节点会发送 PING 消息:一是 redis 每过一秒,节点会选择出集群中随机五个节点,向其中最长时间没有发送过 PING 消息的节点发送 PING。二是某个节点发现集群中有节点的最长时间没有发送过 PING 消息的时间已经超过自己的 cluster-node-timeout 设置时长的一半,节点会向其发送消息

3,PONG 消息,该消息主要用于回应 PING 消息与 MEET 消息,只有完整发送 PING 以及回传 PONG 才算一次完整的心跳。它还可以用来刷新集群中其他节点对自己的认知,比如故障转移完成后,可以直接广播一条 PONG 来告诉其他节点自己成为了主节点

4,FAIL 消息,当节点判断另外一个节点进入 FAIL 状态的时候,会向集群中广播 FAIL 消息,所有接受到的节点将会将这个下线的节点标记为已下线

5,PUBLISH 消息,当节点接受到该命令时,会执行该命令并且广播该消息,这就是 gossip 协议需要做的事情

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Celery是一个Python分布式任务队列框架,而Redis是一个高性能的键值存储数据库。当它们结合在一起时,可以构建一个强大的分布式任务队列系统。 Celery和Redis集群的结合可以提供以下功能: 1. 异步任务处理:Celery可以将任务异步地发送到Redis集群中,然后由工作节点处理。这样可以避免任务阻塞主线程,提高系统的响应速度。 2. 分布式任务调度:Redis集群可以作为Celery的消息代理,负责存储和传递任务消息。多个Celery工作节点可以从Redis集群中获取任务,并进行并行处理。 3. 任务结果存储:Celery可以将任务的执行结果存储在Redis集群中,以便后续查询和使用。 4. 任务队列监控:Redis集群可以提供监控和管理Celery任务队列的功能,例如查看队列长度、清理过期任务等。 为了搭建Celery和Redis集群,你需要进行以下步骤: 1. 安装和配置Redis集群:根据你的需求,可以选择使用Redis Sentinel或Redis Cluster来搭建Redis集群。配置好集群后,确保所有节点都正常运行。 2. 安装和配置Celery:使用pip安装Celery库,并在Celery配置文件中指定Redis集群的连接信息。 3. 编写任务代码:定义你的任务函数,并使用Celery的装饰器将其注册为Celery任务。 4. 启动Celery工作节点:在每个工作节点上启动Celery的工作进程,它们将从Redis集群中获取任务并执行。 5. 发布和调度任务:在你的应用程序中,使用Celery的API将任务发布到Redis集群中,并设置任务的调度规则。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值