上一篇中,我们知道可以通过主从复制+Sentinel的方式实现高可用。但是副本实例其实是不对外服务的,因此本质上,我们所有的数据都会存在master实例上。而一个实例的内存是有限的,这时就需要进行拓展。一般有两种方案,一种是扩大服务器的内存,一种就是进行数据分片。
直接扩大内存的优点在于简单,但是也存在问题。首先内存不能无限扩大,其次,由于RDB的持久化机制是通过fork子进程实现的,在fork的时候是会阻塞Redis主进程的,而fork的用时与数据量是成正比的。所以为了Redis的性能,单实例的内存用量也不宜过大。
Redis Cluster是Redis官方提供的数据分片的解决方案。它是去中心化的,每个节点负责整个集群的一部分数据,每个节点并不一定是均分的。这三个节点相互连接组成一个对等的集群,它们之间通过一种特殊的二进制协议相互交互集群信息。
Redis Cluster将所有数据划分为16384的slots,每个节点负责其中一部分槽位。槽位的信息存储于每个节点中,当 Redis Cluster 的客户端来连接集群时,它也会得到一份集群的槽位配置信息。这样当客户端要查找某个 key 时,可以直接定位到目标节点。
为啥是16384呢?主要有三个原因:如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大;redis的集群主节点数量基本不可能超过1000个;槽位越小,节点少的情况下,压缩率高。具体可以看这篇文章

槽位定位算法

Cluster 默认会对 key 值使用 crc16 算法进行 hash 得到一个16 bit的整数值,然后用这个整数值对 16384 进行取模来得到具体槽位。

槽位迁移感知

集群中,Redis实例与slot的对应关系并不是一成不变的,当增减实例或者为了负载均衡进行重新分配时,实例之间同步完新的槽位信息后,需要有一种机制让客户端也能感知到这种变化。
Cluster 有两个特殊的 error 指令,一个是moved,一个是asking
第一个moved是用来纠正槽位的。如果我们将指令发送到了错误的节点,该节点发现对应的指令槽位不归自己管理,就会将目标节点的地址随同 moved 指令回复给客户端通知客户端去目标节点去访问。这个时候客户端就会刷新自己的槽位关系表,然后重试指令,后续所有打在该槽位的指令都会转到目标节点。
第二个asking指令和moved不一样,它是用来临时纠正槽位的。如果当前槽位正处于迁移中,指令会先被发送到槽位所在的旧节点,如果旧节点存在数据,那就直接返回结果了,如果不存在,那么它可能真的不存在也可能在迁移目标节点上。所以旧节点会通知客户端去新节点尝试一下拿数据,看看新节点有没有。这时候就会给客户端返回一个asking error携带上目标节点的地址。客户端收到这个asking error后,就会去目标节点去尝试。客户端不会刷新槽位映射关系表,因为它只是临时纠正该指令的槽位信息,不影响后续指令。

Redis Cluster 主从模式

Cluster的高可用也是通过主从复制保证的,如果没有复制机制,那么当某个节点发生故障,那么某个范围的slot就无法再提供服务了,当大多数节点都发生故障,集群将停止运行。
Cluster的主从切换不需要Sentinel的支持。

Redis 集群一致性保证

Redis Cluster 无法保证强一致性。这意味着在某些情况下,Redis Cluster 可能会丢失系统已向客户端确认的写入。Redis Cluster 会丢失写入的第一个原因是因为它使用异步复制;另一个原因是可能发生网络分区
我们以6个节点集群为例,由A、B、C、A1、B1、C1组成,有3个master和3个slave。还有一个客户端,我们将其称为 Z1。
分区发生后,可能在分区的一侧有 A、C、A1、B1、C1,而在另一侧有 B 和 Z1。
Z1 仍然能够写入 B,B 将接受其写入。如果分区在很短的时间内恢复,集群将继续正常运行。但是,如果分区持续足够的时间让 B1 在分区的多数侧提升为 master,则 Z1 同时发送给 B 的写入将丢失。
为了减轻这种情况造成的数据不一致性,可以配置节点超时。节点超时后,主节点被视为发生故障,并且可以由其副本之一替换。类似地,在节点超时后,主节点无法感知大多数其他主节点,它会进入错误状态并停止接受写入。

参考