Redis:独立功能的实现

主从复制7-5

1566436936418

主从模式:

  • 主、从节点都可以挂从节点。
  • 最终一致性。

广播模式:

  • 全量同步:

  • 传递RDB文件&restore命令重建kv。

  • 传递在RDB dump过程中的写入数据。

部分同步:

  • 根据offset传递积压缓存中的部分数据。

基础

Redis中用户可以通过执行SLAVEOF命令或设置slaveof选项,让一个服务器去复制另一个服务器,即被复制的是主服务器,对主服务器进行复制的服务器则被称为从服务器。

1
127.0.0.1:12345>Slaveof 127.0.0.1 6379

则12345将成为6379的从服务器,6379成为12345的主服务器。进行复制中的主从服务器双方的数据库将保存相同的数据,概念上将这种现象称作“数据库状态一致”。

当在6379上删除数据,则12345上的键也应该会被删除。

适用性

  • 数据副本。
  • 读写分离,扩展读性能。
  • QPS瓶颈。

概述

为了解决旧版复制概念在处理断线重复制情况时的抵消问题,Redis在2.8开始使用Psync命令代替sync命令来执行复制时的同步操作。

Psync命令具有完整重同步和部分重同步两种模式:

  • 其中完整重同步用于处理初次复制情况:完整重同步的执行步骤和SYNC命令的执行步骤基本一样。
    • 让主服务器创建并发送RDB文件,以及向从服务器发送保存在缓冲区里面的写命令来进行同步。
  • 部分重同步用于处理断线后重复制情况:当从服务器在断线后重新连接主服务器时,如果条件允许,主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器,从服务器只要接收并执行这些命令即可更新至主服务器的状态。

1566836652873

psync不需要重新生成、传送、载入整个RDB文件,执行速度很快。

部分重同步

部分重同步功能由以下三个部分构成:

  • 主服务器的复制偏移量和从服务器的复制偏移量。
  • 主服务器的复制积压缓冲区。
  • 服务器的允许ID。

复制偏移量

执行复制的双方——主服务器和从服务器会分别维护应该复制偏移量:

  • 主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量的值加上N。
  • 从服务器每次受到主服务器传播来的N个字节的数据时,就将自己的复制偏移量加上N。

则对比主从服务器的复制偏移量,程序可以知道主从服务器是否处于一致状态。

当从服务器断线重连,向主服务器发送Psync命令,报告当前的复制偏移量,那么主服务器要决定当前要执行完整重同步还是部分重同步,这与复制积压缓冲区有关。

复制积压缓冲区

复制积压缓冲区是由主服务器维护的有关固定长度的FIFO队列,默认大小为1MB。

当主服务器进行命令传播时,不仅会将写命令发送给所有从服务器,还会将写命令入队到复制积压缓冲区里面。

复制积压缓冲区会为队列中的每个字节记录相应的复制偏移量。如果从服务器发来Psync则:

  • 如果offset偏移量之后的数据仍然存在复制积压缓冲区内,则主服务器将对从服务器执行部分重同步操作。
  • 否则执行完整重同步。

服务器运行ID

部分重同步需要用到服务器运行ID:

  • 每个Redis服务器,不论主从,都会有自己的运行ID。
  • 运行ID在服务器启动时自动生成,由40个随机的16进制字符组成。

当从服务器对主服务器进行初次复制时,主服务器会将自己的运行ID传送给从服务器,从服务器会将这个运行ID保存起来,当从服务器断线重连,会向主服务器发送之前保存的运行ID。

  • 如果从服务器保存的运行ID和当前连接的主服务器运行ID相同,那么说明从服务器断线前复制的就是当前连接的这个主服务器,主服务器可以继续尝试执行部分重同步操作。
  • 相反,如果不同,则需要进行完整重同步。

Psync实现

Psync调用方法有两种:

  • 如果从服务器之前没有复制过任何主服务器,或者执行执行过Slaveof no one,则从服务器在开始新的一次复制时将向主服务器发送Psync ? -1命令,主动请求主服务器进行完整重同步。
  • 相反,则会向主服务器发送 Psync <runid> <offset>。即传递运行Id与偏移量。

主服务器返回有三种:

  • +Fullresync <runid> <offset>。标识主服务器将与从服务器执行完整重同步操作。runid是主服务器运行ID,offset是当前主服务器的偏移。
  • +Continue。标识主服务器将与从服务器执行部分重同步,从服务器等待主服务器发送缺失数据。
  • -ERR,表示主服务器版本低于2.8,无法识别。

复制的实现

1
slaveof <master_ip> <master_port>
  • 设置主服务器的地址和端口

当客户端发送slaveof命令,则会将客户端给定的主服务器IP地址以及端口保存到服务器状态的masterhost属性与masterport当中:

1
2
3
4
5
struct redisServer{
char *masterhost;

int masterport;
}
  • 建立套接字连接

当执行slaveof命令执行后,从服务器将根据命令设置的IP地址和端口创建连向主服务器的套接字连接。

如果连接成功建立,则从服务器将为该套接字管理有关专门用于处理复制工作的文件事件处理器,这个处理器将负责执行后续的复制工作。

主服务器接收从服务器的套接字连接后,将为该套接字创建相应的客户端状态,并将从服务器看作是一个连接到主服务器的客户端来对待。

  • 发送Ping命令

从服务器成为主服务器的客户端后,第一件事情就是Ping。

Ping可以使得确定套接字的读写是否正常、检查主服务器是否能够正常处理命令请求。

如果主服务器返回了一个命令回复,但是从服务器不能在规定事件内读取命令回复,则表示网络不稳定。此时从服务器断开并重新创建连向主服务器的套接字。

如果主服务器向从服务器返回一个错误,表示主服务器暂时没有办法处理从服务器的命令请求,不能执行后续操作,此时从服务器断开并重新创建连向主服务器的套接字。

如果从服务器读取到PONG,则表示连接正常可以继续执行。

  • 身份验证

如果从服务器设置了masterauth,则进行身份验证。

当需要进行身份认证,则从服务器向主服务器发送一条Auth命令,参数为从服务器masterauth的值。

1566838990185

  • 发送端口信息
1
replconf listening-port <port-number>

向主服务器发送从服务器的监听端口号。

主服务器接收并将端口号记录在从服务器对应的客户端状态的slave_listening_port属性中。

  • 同步

从服务器向主服务器发送Psync,执行同步操作,将自己的数据更新到主服务器当前的状态。

  • 命令传播

完成同步后进入命令传播状态,此时主服务器只要一直将自己执行的写命令发送给从服务器,而从服务器只要一直接收并执行主服务器发来的写命令即可保证一致性。

心跳检测

在命令传播阶段,从服务器默认会以1/s的频率向主服务器发送命令Replconf ack <replication_offset>。其中replication_offset是从服务器当前的复制偏移量。

Replconf ack的作用:

  • 检测主从服务器的网络连接状态。
    • 如果主服务器超过1S没有接收到命令,则主服务器知道连接出现问题了。
  • 辅助实现min-slaves。可以防止主服务器在不安全的情况下执行写命令。
    • min-slaves-to-write。从服务器数量小于3个时拒绝执行写命令。
    • min-slaves-max-lag。延迟值都大于该值时,拒绝执行写命令。
  • 检测命令丢失。
    • 如果由于网络故障,主服务器发送的命令在半路丢失,则主服务器将发觉从服务器当前的复制偏移量少于自己,则会从复制积压缓冲区里找到从服务器缺少的数据,并将数据重新发送给从服务器。

Sentinel

哨兵是Redis高可用的解决方案:由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。

1566839386541

当server1下线后,主从复制操作将终止,并且Sentinel系统会检测到server已经下线。

当下线时长超过用户设定的下线时长上限时,Sentinel系统就会对server1执行故障转移操作:

  • 首先挑选一个从服务器,并将该从服务器升级为新的主服务器。
  • 之后Sentinel系统会向server1属下的所有从服务器发送新的复制指令,让它们成为新的主服务器的从服务器,当所有从服务器都开始复制新的主服务器时,故障转移完毕。
  • Sentinel继续监视已下线的server1,并在它重新上线时设置为新的主服务器的从服务器。

启动并初始化Sentinel

启动一个Sentinel可以使用命令

1
redis-sentinel /path/to/your/sentinel.conf

当一个Sentinel启动需要执行以下步骤:

  • 初始化服务器。
    • Sentinel本质上是一个运行在特殊模式下的Redis服务器,所以启动第一步就是初始一个普通的Redis服务器。
    • 但是Sentinel不会使用数据库,在启动时不会载入RDB或AOF。
  • 将普通Redis服务器使用的代码替换成Sentinel专用代码。
    • 普通Redis服务器使用redis.h/REDIS_SERVERPORT作为端口,Sentinel使用sentinel.c/REDIS_SENTINEL_PORT作为端口。
    • 普通Redis使用redis.c/redisCommandTable作为命令表,sentinel使用sentinel.c/sentinelInfoCommand作为命令表。因此该服务器不能执行set、dbsize等命令。
  • 初始化Sentinel状态。
    • 初始化sentinel.c/sentinelState,保存了服务器中所有与Sentinel功能有关的状态。
  • 根据给定的配置文件,初始化Sentinel的监视主服务器列表。
    • Sentinel状态的masters字典记录了所有被Sentinel监视的主服务器的相关信息。键为主服务器名字,值为被监视主服务器对应的sentinel.c/sentinelRedisInstance结构。代表一个Redis服务器实例,可以是主从服务器或另一个Sentinel。
  • 创建连向主服务器的网络连接。
    • Sentinel将成为主服务器去的客户端,可以向主服务器发送命令并从命令回复中获取相关信息。
    • Senitinel会创建两个与主服务器的异步网络连接(为了与多个实例创建多个网络连接)。
      • 命令连接,专门用于向服务器发送命令,并接收命令回复。
        • Sentinel除了订阅频道,还必须向主服务器发送命令来与主服务器通信。
      • 订阅连接,专门用于订阅主服务器的_sentinel_:hello频道。该连接是为了不丢失该频道的任何信息。
        • Redis发布与订阅功能中,被发送的信息不会保存在Redis服务器中,因此为了不丢失任何频道信息,必须进行订阅。

获取主服务器信息

Sentinel默认以1/10s的频率通过命令连接向被监视的主服务器发送INFO命令,并分析回复来获得主服务器的当前信息。

  • 主服务器本身的信息,runid,服务器角色等。
  • 主服务器属下所有从服务器的信息,每个都以slave开头,记录ip、port等地址。

获取从服务器信息

当Sentinel发现主服务器有新的从服务器出现时,Sentinel除了会为这个新的从服务器创建相应的实例结构外还会创建连接到从服务器的命令连接和订阅连接。

默认以1/10s向从服务器发送INFO命令并分析回复信息:

  • 从服务的runid。
  • 从服务器的角色role,即slave。
  • 主服务器的IP、port。
  • 主从服务器的优先级master_link_sattus。
  • 从服务器的优先级slave_priority。
  • 从服务器的复制偏移量。

向主服务器和从服务器发送信息

默认情况下Sentinel以1/2s通过命令连接向所有被监视的主从服务器发送以下格式的命令:

1
publish _sentintl_:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"

即向服务器的_sentinel_:hello频道发送一条消息。

接收来自主服务器和从服务器的频道信息

Sentinel对_sentinel_:hello频道的订阅一直持续到连接断开位置。即将一直订阅、发送该频道的消息。

对于监视同一个服务器的多个Sentinel,一个Sentinel发送的信息会被其他Sentinel接收到,用于更新其他Sentinel对发送信息Sentinel的认知。

更新Sentinels字典

Sentinel为主服务器创建的实例结构中的sentinel字典中不仅包含本身,还包含所有同样监视这个主服务器的Sentinel资料。

因为一个Sentinel可以通过分析频道信息来获知其他Sentinel存在,因此使用Sentinel时不需要提供各个Sentinel的地址。

创建连接向其他Sentinel的命令连接

当发现了一个新的Sentinel,还会出现一个连向新的Sentinel的命令连接,新的Sentinel也会与原来的连接,最终监视同一主服务器的多个Sentinel将会形成相互连接的网络。

它们之间可以发送命令请求来进行信息交换,下线检测当中都会使用到该命令连接进行通信。但是它们间不会创建订阅连接。

1566869455846

检测下线状态

主观下线状态

默认情况下,Sentinel会1/s频率向所有与它创建了命令连接的实例发送PING,并通过回复来判断实例是否在线。

  • 有效回复:+PONG,-LOADING,-MASTERDOWN。
  • 无效回复:其他回复或没有回复。

Sentinel配置文件中的down-after-milliseconds指定了Sentinel判断实例进入主观下线所需的时间长度。超时返回无效回复则该实例主观下线。

客观下线状态

当Sentinel将一个主服务器判断为主观下线后,为了确认是否真的下线,它会向监视这一主服务器的其他Sentinel进行询问,看它们是否也认为主服务器已经下线,如果得到足够数量的确认,则将从服务器判定为客观下线,并对主服务器进行故障转移。

选举

当主服务器被判断为客观下线时,监视这个下线服务器的各个Sentinel会进行协商选举出一个领头Sentinel,并由领头Sentinel对下线主服务器进行故障转移操作。

选举领头Sentinel的规则和方法:

  • 所有在线的Sentinel都有被选为领头Sentinel的资格。
  • 每次进行领头Sentinel选举后,不论选举是否成功,所有Sentinel的配置纪元的值都会自增一次。
  • 在一个配置纪元中,所有Sentinel都有一次将某个Sentinel设置为局部领头Sentinel的机会,并且聚合领头一旦设置,在这个配置纪元当中就不会修改。
  • 每个发现主服务器进入客观下线的Sentinel都会要求其他Sentinel将自己设置为局部领头Sentinel。
  • 当一个Sentinel向另一个Sentinel发送SENTINEL is-master-down-by-addr命令,并且命令中runid不是*而是源Sentinel的runid,则表示要求目标Sentinel将前者设置为局部领头Sentinel。
    • 设置规则为先到先得。
    • 确认设置后将返回命令回复,记录目标Sentinel的局部领头runid与配置纪元。
  • 源Sentinel接收到命令回复会检测runid与配置纪元与自己是否相同,如果一致则成为局部领头。
    • 如果某个Sentinel被半数以上的Sentinel设置为局部领头,则成为领头Sentinel。
    • 如果没有一个Sentinel被选举成功,则在一段时间后再次选举,直到选出为止。

故障转移

  • 在已下线主服务器属下的所有从服务器里,选出一个从服务器并将其转换为主服务器。
  • 让已下线主服务器属下的所有从服务器改为复制新的主服务器。
  • 将已下线主服务器设置为新的主服务器的从服务器。

选出新的主服务器

即挑选出一个完好的从服务器,并向该从服务器发送SLAVEOF on one,将这个从服务器转换为主服务器。挑选方式:

  • 领头Sentinel将已经下线的主服务器的所有从服务保存到一个列表中,根据规则一项一项的过滤。
    • 删除列表中所有下线或断线的从服务器。保证剩余服务器都是正常在线的。
    • 删除列表中所有最近5s内没有回复领头Sentinel的INFO命令的从服务器,保证剩余服务器都是最近成功通信的。
    • 删除所有与已下线主服务器连接断开超过down-after-milliseconds * 10毫秒的从服务器,保证剩余服务器都没有过早与主服务器断开连接,即数据较新。
  • 根据从服务器的优先级,对剩余服务器进行排序,选出最高的。
    • 如果有多个相同优先级,则按照复制偏移量进行排序,选出偏移量最大的。
    • 如果依然相同,则按照ID排序,选出最小。

修改从服务器的复制目标

向从服务器发送SLAVEOF命令来实现。

redis集群

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

节点

要组建一个真正可用的集群,我们必须将各个独立的节点连接起来,构成一个包含多个节点的集群。连接各个节点的工作可通过Cluster meet完成:

1
Cluster meet <ip> <port>

向一个节点node发送Cluster meet命令,可用让node节点与ip、port指定的节点进行握手,当握手成功后,node节点就会将ip和port指定的节点添加到node节点所在的集群中。

使用Cluster node可用查看集群目前包含的节点。

启动节点

一个节点就是一个运行在集群模式下的Redis服务器,Redis服务器在启动时会根据cluster-enabled配置选项是否为yes来决定是否开启服务器的集群模式。

节点会继续使用所有在单机模式中使用的服务器组件。对于集群模式下的数据,节点保存在了cluster.h/clusterNode、cluster.h/clusterLink、cluser.h/clusterState中。

集群数据结构

clusterNode保存了一个节点的当前状态,如创建时间、名字、当前的配置纪元、节点IP、port。

Cluster meet

当向节点A发送Cluster meet,节点A将与节点B进行握手,以此来确认彼此的存在:

  • 节点A会为节点B创建一个ClusterNode,并将改结构添加到自己的ClusterState.nodes字典中。
  • 之后,节点A根据Cluster Meet命令给定的IP、port向节点B发送一条Meet消息。
  • 如果一切顺利,节点B接收到Meet,节点B会为节点A创建一个ClusterNode结构,并将该结构添加到自己的ClusterState.nodes字典中。
  • 节点B向节点A返回一条Pong消息。
  • 如果一切顺利,节点A接收Pong消息,即节点A知道节点B已经确认了Meet。
  • 之后节点A向节点B返回一条Ping。
  • 节点B接收Ping消息,即节点B知道节点A已经确认了Pong。握手完成。

槽指派

Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个slot,数据库中的每个键都属于这16384个槽中的一个,集群中的每个节点可用处理0-16384个槽。

  • 当数据库中的16384个槽都有节点在处理,集群处于上线状态。
  • 否则有任何一个槽没有得到处理,则集群处于下线状态。

使用Cluster addslots可将一个或多个槽指派给节点负责:

1
Cluster addslots <slog> [slot ..]

当槽全部指派完成后,集群上线。

记录节点的槽指派信息

clusterNode的slots与numslot属性记录了节点负责处理哪些槽。

slots是一个二进制数据,长度16384/8=2048字节。

传播节点的槽指派信息

一个节点还会将自己负责处理的槽slots通过消息发送给集群中的其他节点,以此告诉其他节点自己目前负责处理哪些槽。

当接收到其他节点的slots,则会更新节点自己的clusterState.nodes的其他节点的slots。

因此clusterState.slots当中记录了整个集群的所有槽指派信息。

Cluster addslots

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def cluster_addslots(*all_input_slots){
//遍历所有输入槽,检查他们是否都是未指派槽
for i in all_input_slots{
//如果有任何一个槽已经被指派,则error
if(clusterState.slots[i] != NULL){
reply_error();
return
}
}
//再次遍历,将槽指派给当前节点
for i in all_input_slots{
clusterState.slots[i]=clusterState.myself;
setSlotBit(cluserState.myself.slots,i);
}
}

在集群中执行命令

当集群上线后,客户端就可向集群中的节点发送数据命令了。

当客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查这个槽是否指派给了自己。

  • 如果是指派给了自己,则节点将直接执行这个命令。
  • 否则节点向客户端返回有关Moved错误,指引客户端转向正确的节点,再次发送之前想要执行的命令。

计算键属于哪个槽:

1
2
3
def slot_number(key){
return CRC16(key) & 16383
}

Moved错误

节点发现键所在的槽并非由自己负责处理时,节点向客户端返回Moved错误,引客户端转向正确的节点。

1
Moved <slot> <ip>:<port>

当客户端接收到错误时,会根据错误中提供的IP和PORT转向至负责处理slot的节点,并向其重发命令。

节点数据库的实现

单机与节点的数据库区别是,节点只能使用0号数据库,单机Redis服务器没有这一限制。

节点使用clusterState结构中的slots_to_keys跳跃表来保存槽与槽之间的关系。

重新分片

Redis集群的重新分片可将任意数量已经指派给某个节点的槽改未指派给另一个节点,并将相关槽所属的键值对从源节点转移到目标节点。

重新分片操作可在线进行,重新分片过程中集群不需要下线,并且源节点与目标节点都可以继续处理目标请求。

原理

Redis集群的重新分片操作是由Redis集群管理软件redis-trib负责执行的,Redis提供了进行重新分片所需的所有命令,而redis-trib则通过源节点和目标节点发送命令来进行重新分片操作。

  • redis-trib对目标节点发送cluser setslot <slot> importing <source_id>,让目标节点准备号导入源节点当中槽slot的键值对。
  • redis-trib对源节点发送cluster setslot <slot> migrating <target_id>,让源节点准备好将属于槽slot的键值对迁移至目标节点。
  • redis-trib向源节点发送cluster getKeyInSlot <slot> <count>,获得最多count个属于槽slot的键值对的键名。
  • 对于获得的每个键名,redis-trib都向源节点发送migrate <target_ip> <target_port> <key_name> 0 <timeout>将被选中的键原子地迁移至目标节点。
  • redis-trib向集群中任意节点发送cluster setslot <slot> node <target>,将槽指派给目标节点,并通知了整个集群。

ASK错误

在重新分片期间可能存在ASK错误:属于被迁移槽的一部分键值对保存在源节点中,而另一部分保存在目标节点中。

而当客户端向源节点发送一个与数据库有关的命令,并且命令要处理的数据库键恰好属于正在被迁移的槽时。

  • 源节点首先在自己的数据库中查找,如果找到则返回。
  • 如果没有找到,则可能已经迁移到目标节点,则向客户端返回一个ASK错误,指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令。
    • ASK错误,打开发送该命令客户端的Redis_Asking标识。是一种迁移槽的临时措施。

1566957453276

复制与故障转移

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

设置从节点

1
cluster replicate <node_id>

可让接收命令的从节点成为node_id的从节点并开始对主节点进行复制。

  • 接收到命令的节点首先会在自己的clusterState.nodes字典中找到node_id对应节点的clusterNode结构,并将自己的clusterState.myself.slaveof指针指向这个结构,以记录这个节点正在复制的主节点。
  • 节点修改自己在clusterState.myself.flags中的属性,关闭Redis_node_master标识,打开redis_node_slave标识,即成为了从节点。
  • 最后调用复制代码,复制slaveOf指向的主节点。与主从复制一致。

故障检测

  • 集群中的每个节点都会定期地向集群中的其他节点发送PING,以此来检测对方是否在线。
    • 如果接收Ping消息的节点没有在规定的时间内,向发送PING消息的节点返回PONG,那么发送Ping消息的节点就会将接收PING消息的节点标记为疑似下线。
  • 集群中的各个节点会通过互相发送消息的方式来交换集群中的各个节点的状态信息,例如在线、疑似下线PFail、已下线Fail。

当一个主节点A通过消息得知主节点B认为主节点C进入了PFail,则主节点A会找到clusterState.nodes中C节点,并将B的下线报告添加到clusterNode结构的fail_reports链表中。

如果一个集群中,半数以上负责处理槽的主节点都将某个主节点X报告未疑似下线,那么这个主节点X将标记为已下线,将主节点X标记为已下线的节点会向集群广播一条关于主节点X的Fail消息。

故障转移

当一个从节点发现自己正在复制的主节点进入了已下线状态,从节点将开始对下线主节点进行故障转移:

  • 复制下线主节点的所有从节点里面,会有一个从节点被选中。
  • 被选中的从节点会执行Slaveof no one命令,成为新的主节点。
  • 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。
  • 新的主节点向集群广播一条PONG消息,通知其他节点该节点已经成为了主节点。
  • 新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成。

选举

  • 集群的配置纪元是一个自增计数器。
  • 当集群里的某个节点开始一次故障转移操作时,集群配置纪元的值++。
  • 对于每个配置纪元,集群里的每个负责处理槽的主节点都有一次投票的机会,而第一次向主节点要求投票的从节点将获得主节点的投票。
  • 当从节点发现自己正在复制的主节点进入已下线状态,从节点会向集群广播一条Clustermsg_type_faillover_auth_request消息,要求所有收到消息并具有投票权的主节点向这个从接待你投票。
  • 如果一个主节点具有投票权,并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条Clustermsg_type_fallover_auth_ack消息,表示这个主节点支持从节点成为新的主节点。
  • 每个参与选举的从节点都会接收Clustermsg_type_failover_auth_ack消息,并根据收到多少条消息统计自己获得了多少主节点的支持。
  • 如果集群中有N个具有投票权的主节点,则当收集到n/2+1张支持,则成为新的主节点。
  • 如果没有收集到足够多的支持票,则进入一个新的配置纪元,再次选举直到选出新的主节点为止。

消息

集群中的各个节点通过发送消息和接收消息来进行通信。

消息的类型

节点发送的消息有:

  • Meet消息。请求接收者加入到发送者当前所处的集群里。
    • 当发送者接收到客户端发送的Cluster meet命令时,发送者会向接收者发送Meet消息,请求接收者加入到发送者当前所处的集群里。
  • Ping消息。检测节点是否在线。
    • 集群里的每个节点默认1/S就会从已知节点列表中随机选出5个节点,然后对这5个节点中最长时间没有发送过Ping消息的节点发送Ping,检测节点是否在线。
    • 若节点A最后一次收到节点B发送的Pong消息的时间,距离当前时间已经超过了节点A的cluster-node-timeout时长的一般,则节点A也会向B发送Ping,防止因为长时间没有随机选中导致更新落后。
  • Pong消息。向发送者确认Ping、Meet消息已经送达。
    • 当接收者收到Meet、Ping都会向发送者返回一条Pong。
    • 也可向集群广播自己的Pong以让集群中其他接待你立即刷新关于这个节点的认知。
  • Fail消息。判断是否Fail。
    • 当主节点A判断另一个主节点B已经进入Fail,则会广播一条B的Fail消息。
  • Publish消息。广播消息。
    • 当收到Publish,节点执行该命令,并向集群广播一条Publish,所有接收到publish消息的节点会执行相同的Publish。

消息的组成

header:由一个cluster.h/clusterMsg表示

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
typedef struct{
//消息的长度,包含消息头长度与正文长度
uint32_t totlen;
//消息的类型
uint16_t type;
//消息正文包含的节点信息数量,只在meet、ping、pong这三种Gossip协议消息使用
uint16_t count;
//发送者所处的配置纪元
uint64_t currentEpoch;

//如果发送者是主节点,记录发生者的配置纪元。如果是从节点,记录复制的主节点的配置纪元
uint64_t configEpoch;
//发送者ID
char sender[Redis_cluster_namelen];
//发生者目前的槽指派信息
unsigned char myslots[redis_cluster_slots/8];
//如果发送者是从节点,记录复制的主节点的名字。如果是主节点,记录Redis_node_null_name。
char slaveof[redis_cluster_namelen];
//发送者PORT
uint16_t port;
//发送者标志值
uint16_t flags;
//发送者所处集群状态
unsigned char state;
//消息正文
union clusterMsgData data;
}

data:指向联合cluster.h/clusterMsgData。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
union clusterMsgData{
//Meet、Ping、Pong
struct {
//包含两个gossip结构
clusterMsgDataGossip gossip[1];
}
//fail
struct{
clusterMsgDataFail msg;
}fail;

struct{
clusterMsgDataPublish msg;
}publish:
}

Gossip

Redis集群中各个节点通过Gossip协议来交换各自关于不同节点的状态信息,其中Gossip协议由Meet、Ping、Pong三种消息实现。

基于gossip协议,去中心化。

节点间两两通信,有节点数量上限。

每次发送一条该消息,发送者都从自己的已知节点列表中随机选出两个节点,并会将两个被选择节点分别保存在两个clusterMsgDataGossip当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct{
//节点的名字
char nodeName[redis_cluster_namelen];
//最后一次向该节点Ping消息的时间戳
uint32_t ping_sent;
//最后一次从该节点收到Pong消息的时间戳
uint32_t pong_received;
//节点的IP
char ip[16];
//节点的断开
uint16_t port;
//节点的标识值
uint16_t flags;
}

当接收者收到Meet、ping、pong时,接收者会访问消息正文中的两个clusterMsgDataGossip结构,并根据自己是否认识结构中记录的节点来选择操作:

  • 如果不存在接收者的已知节点列表中,那么说明时第一次接触,需要进行握手。
  • 如果已经存在,则根据clusterMsgDataGossip信息进行更新对于的clusterNode。

Fail

Gossip消息存在延迟,需要一定时间才能传播到整个集群。为了尽快故障转移,因此使用Fail。

1
2
3
typedef struct{
char nodeName[redis_cluster_namelen]
}clusterMsgDataFail

由于名字唯一,则只有名字即可。

Publish

当客户端向集群中某个节点发送:

1
publish <channel> <message>

则接收到命令的节点不仅会向channel频道发送消息,还会像集群广播一条,使得所有接收到publish消息的节点都会像channel频道发送message。

一致性hash

存在中心。

实例宕机、加节点容易造成数据丢失。

功能

事务

Redis通过MULTI(开始)、EXEC(提交)、WATCH等实现事务功能

事务提供将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,在事务执行期间,服务器不会中断事务执行其他客户端的命令请求。

事务的原子性、一致性、隔离性、持久性 ACID

  • Atomicity(原子性):一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。
  • Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束触发器级联回滚等。
  • Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
  • Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

事务的实现

事务的三个阶段

  • 事务开始
    • MULTI命令
  • 命令入队
  • 事务执行

WATCH

乐观锁,在EXEC命令执行前,监视任意数量的数据库键,并在EXEC命令执行时,检查被监视的键是否至少有一个已经被修改,如果是,则拒绝执行事务,并返回代表事务执行失败的空回复

监视器

使用Monitor命令,客户端可以将自己变为一个监视器,实时接收并打印出服务器当前处理的命令请求的相关信息。

每当一个客户端向服务器发送一条命令请求时,服务器除了会处理这条命令请求外,还会将这条命令请求的信息发送给所有监视器。

成为监视器

1
2
3
4
def monitor();
client.flags = redis_monitor;//打开客户端的监视器标志
server.monitors.append(client);//将客户端添加到服务器状态的monitors链表末尾。
send_reply("OK");//向客户端返回OK

向监视器发送命令信息

服务器每次处理命令请求前,都会调用replicationFeedMonitors函数,由这个函数将被处理的命令请求的相关信息发送给各个监视器。

1
2
3
4
5
6
7
//执行命令的客户端,当前数据库号码,命令参数,命令参数的个数
def replicationFeedMonitors(client,monitors,dbid,agrv,argc);
//创建要发送给各个监视器的信息
msg = create_message(client, dbid, argv, argc);
//遍历监视器并发送信息
for monitor in monitors
send_message()

慢查询

  • 客户端请求的生命周期。
  • 两个配置。
  • 三个命令。

1566350276531

慢查询发生在第3阶段,即命令的执行针对很慢。

客户端超时不一定慢查询,但慢查询是客户端超时的一个可能因素。

批量操作

pipline

流水线操作。如果每次建立一次连接实现,则n次操作需要n次网络连接+n次命令计算。

client:将多个命令缓存起来,缓冲区满了就发送。

redis:处理一个tcp连接法来的多个命令,处理完一个就发一个。

twemproxy:既要处理一个client连接法来的多个命令,又要将到同一个下游redis server的命令缓存起来一起发送。

  • 节省往返时间。
  • 减少了proxy、redis server的IO次数。

mget

弱于pipline。

client:使用mget。

redis:一个命令中处理多个key,等所有key处理完组装完后组装恢复一起发送。

twmproxy:拆key分发到不同redis server,需要等待、缓存mget中全部恢复。

  • 节省往返时间。
  • proxy缓存

发布订阅

发布者、订阅者、通道。

二进制位数组

参考