即使是使用哨兵,此时的redis集群的每个数据库依然存有集群中的所有数据,从而导致集群的总数据存储量受限于可用存储内存最小的节点,形成木桶效应。因为redis是基于内存存储的,所以这个问题尤为突出。
在redis3.0之前,我们是通过在客户端去做分片,通过hash的方式对key进行分片存储。分片虽然能够解决各个节点的存储压力,但是维护成本高,增加、移除节点比较繁琐。因此在3.0之后的版本支持了集群功能,集群的特点在于拥有和单机实例一样的性能,同时在网络分区以后能够提供一定的可访问性以及对主数据库故障恢复的支持。
哨兵和集群是两个独立的功能,当不需要对数据进行分片使用哨兵就够了,如果要进行水平扩容,集群是一个比较好的方式
拓扑结构
一个redis-cluster由多个redis节点构成,不同节点组服务的数据没有交集。
节点组内分为主备两类节点,对应master和slave节点,两者数据准实时一致,通过异步化的主备复制机制来保证。一个节点组有且只有一个master节点,可以有0到多个slave节点,在这个节点组中只有master节点对用户提供写服务,读服务可以由master和slave提供
redis-cluster是基于gossip协议实现的无中心化节点的集群,因为去中心化的架构不存在统一的配置中心,各个节点对整个集群状态的认知是来自于节点之间的信息交互。在redis-cluster中,这个信息交互是通过Redis Cluster Bus来完成的
数据分区
分布式数据库首要解决把整个数据集按照分区规则映射到多个节点的问题,每个节点负责整个数据的一个子集,redis-cluster采用哈希分区规则,采用虚拟槽分区。
虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有的数据映射到一个固定范围内的整数集合中,整数定义为槽(slot)。redis-cluster槽的范围是0 ~ 16383。
槽是集群内数据管理和迁移的基本单位,采用大范围的槽的主要目的是为了方便数据的拆分和集群的扩展,每个节点负责一定数量的槽,计算公式:1
slot = CRC16(key)%16383
如下图所示:
HashTags
通过分片手段,可以将数据合理的划分到不同的节点上,但是有时候,我们希望对相关联的业务以原子方式进行操作,比如:
在单节点上执行MSET,是一个原子操作,但是在集群环境下,它的操作就不是原子操作,是因为多个key可能会被分配到不同的机器上
所以,就会有一个矛盾点,即要求key尽可能的分散在不同机器上,又要求相关联的key分配到相同的机器上,这该如何解决呢?
从之前的分析中可以了解到,分片其实就是一个hash的过程,对key做hash取模后划分到不同的机器上。所以要做到上面这点,需要考虑如何让相关联的key得到的hash值都相同,在redis中引入了HashTags的概念,可以使得数据分布算法可以根据key的某一个部分进行计算,然后让相关的key落到同一个数据分片中。
举个例子:加入对于用户的信息进行存储:user:user1:id、user:user1:name,那么通过hashtag的方式:user:{user1}:id、user:{user1}:name
当一个key包含{}的时候,就不对整个key做hash,而是仅对{}包含的字符串做hash
重定向客户端
Redis Cluster并不会代理查询,那么如果客户端访问了一个key并不存在的节点,该如何处理?比如获取key为msg的值,msg计算出来的槽编号为254,当前节点正好不负责编号为254的槽,那么就会返回客户端下面的信息:1
-MOVED 254 127.0.0.1:6381
表示客户端想要的254槽由运行在IP为127.0.0.1,端口为6381的master示例服务上,如果恰好由当前节点负责,则当前节点会立即返回结果
分片迁移
在一个稳定的redis-cluster下,每一个slot对应的节点是确定的,但是在以下情况节点和分片对应的关系会发生变更:
- 新加入master节点
- 某个节点宕机
也就是说当动态添加或减少节点时,需要将16384个槽做个再分配,槽中的键值也要迁移(这一过程处于半自动状态,需要人工介入)
新增一个主节点:
新增一个节点D,redis-cluster从各个节点的前面各拿取一部分slot到D上,最后大致会变成:
- 节点A覆盖1365 ~ 5460
- 节点B覆盖6827 ~ 10922
- 节点C覆盖12288 ~ 16383
- 节点D覆盖0 ~ 1364,5461 ~ 6826,10923 ~ 12287
删除一个节点:
先将节点的数据移动到其它节点上,然后执行删除
槽迁移的过程
槽迁移的过程中有一个不稳定状态,这个不稳定状态会有一些规则,这些规则定义客户端的行为,从而使得redis-cluster不必宕机的情况下也可以执行槽的迁移。如下图(迁移槽编号为1,2,3的):
简单的工作流程:
- 向Master B发送状态变更命令,把Master B对应的slot状态设置为IMPORTING
- 向Master A发送状态变更命令,把Master A对应的slot状态设置为MIGRATING
当状态变成IMPROTING或者MIGRATING时,对于slot内部数据提供读写服务的行为和通常状态下是有区别的
MIGRATING状态
- 如果客户端访问的key还没有迁移出去,则正常处理这个key
- 如果key已经迁移或者根本就不存在这个key,则回复客户端ASK信息让它跳转到Master B去执行
IMPORTING状态
当来自客户端的正常访问不是从ASK跳转过来的,说明客户端还不知道迁移正在进行,很有可能操作了一个目前还没迁移完成的并且还存在与Master A上的key,如果此时在A上已经修改了,那么B和A的修改则会发生冲突。
对于Master B上的slot所有非ASK跳转过来的操作,Master B都不会去处理,而是通过MOVED命令让客户端调转到Master A上去处理