# 集群

# Redis 切片集群 Redis 3.0+

在数据量很大的情况下,Redis 使用 RDB 进行持久化时会通过 fork 子进程来完成。而 fork 操作会阻塞主进程,而且 fork 操作需要的时间与数据量大小正相关,这样会导致 Redis 响应变慢。

使用切片集群来启动一组 Redis 实例组成一个集群,将数据按照一定规则切分成多份分别保存在不同实例中。对于单个集群来说,数据量变小,Redis 响应时间不会过分变慢。

这样的扩展称为横向扩展。对应打的纵向扩展指升级单个实例的配置资源。

# 一致性哈希算法

一致性哈希算法是麻省理工大学的 David Karger 及其合作者发明的一种特殊的哈希算法。它可以用来解决分布式情况下数据映射的问题。

在这个算法中,将整个哈希值空间组织到了一个虚拟的圆环中。例如哈希空间 , 我们先对服务器节点和数据分别进行 hash 计算来确定它们在圆环上的位置。其中对服务器节点可以使用它们的 IP 或者 名称作为 hash 函数的参数。

这样以来,服务器和所有的数据都被散列到了这个圆环上。之后判断每个数据点所属的服务器方法是:从数据点开始顺时针旋转,遇到的第一个服务器节点就是它会被存放的服务器节点。

为了防止服务器节点过于集中而造成数据倾斜,引入虚拟节点的概念。对每个服务器计算多个 hash, 这些位置成为虚拟节点,它们都对应着同一个真实节点。通过这样引入众多节点的方式来使得数据尽可能的分布均匀。

# Redis Cluster

在 Redis 中提供了 Redis Cluster 方案来支持切片集群。它实现数据与 Redis 实例之间关系的方式不完全等同一致性哈希算法,它采用了哈希槽的概念。

数据映射
在一个切片集群中一共拥有 16384 个哈希槽。这些槽类似数据分区,每个数据都会根据 key 被映射到一个哈希槽中。逻辑为:利用 key, 将它按照 CRC16 算法得到一个 16 位的值。再将这个值对 16384 取模,得到一个 0 到 16384 之间的数字。这个数字就是这个数据会被映射到的哈希槽编号。

当使用 cluster create 命令来创建集群时,哈希槽与具体 Redis 实例之间的对应是平均的。当集群中有 n 个 Redis 实例时,每个实例负责 个哈希槽。
当使用 cluster meet 命令手动建立实例之间的连接来形成集群时,可以使用 cluster addslot 命令来给每个实例指定哈希槽数量。但是手动指定必须将 16384 个槽都分配好,否则集群无法正常工作。

客户端定位数据
当 Redis 建立好集群后,具体实例会将自己所负责的槽信息广播给相连的其他实例,这样当客户端和集群建立连接后访问其中一个实例就能知道所有实例负责的槽信息。之后客户端会将哈希槽信息缓存到本地。当客户端请求键值对时,先根据键来计算哈希槽,再根据槽信息找到对应的实例并发送请求。

每一个 Redis 实例都会维护一个 16384 位的二进制位序列和一个大小为 16384 的共享数组。当一个实例需要负责哈希槽 k 时,二进制位序列中槽道序号对应的位,第 k 位,会被赋值为是 1. 共享数组被所有实例共同维护。
集群中共同维护的数组中保存了数组下标对应的槽道由哪个实例负责。实例的 ip 和端口号会被保存到数组中。

重分片
当集群发生变动时,哈希槽会发生重分片(Reshard)操作。

  • 集群中 Redis 实例发生增减;
  • 负载均衡。

当发生重分片时,实例负责的槽道会发生变化。具体数据和槽道一一对应,所以在重分片后数据存放根据槽道变动。槽道发生变动的数据在对 key 计算哈希取模后找到新的槽道号,并进行移动到新的实例中。实例之间广播槽信息获得新的整体槽信息。
具体过程:

  • 确定新的哈希槽归属
  • 减少槽道的实例中位序列变化,不需要的被标为 0
  • 新增槽道的示例中位序列变化,需要的被标记为 1
  • 共享数组中清空减少槽道实例中负责的被减少的槽道下标数据
  • 共享数组中赋值增加槽道实例中负责的被增加的槽道下标数据,内容为自己的 ip 和端口号
  • 确定数据归属,所以 key 重新计算,根据转发逻辑将数据传输到对应的实例中

重定向机制
在服务器端变动完成后,客户端并没有拿到最新的槽道与数据信息,所以当客户端对某实例进行数据请求时,可能会发生重定向。具体分为两种情况:

  1. 当前实例中数据迁移结束,已经完全被移动到另一个实例中
    Redis 会返回一个 MOVE 错误信息,包含了该数据所在新实例的信息。客户端可以和新实例进行连接获取数据,并把更新后的该槽道与实例的对应关系缓存进行更新。
  2. 当前实例中数据正在迁移,只有一部分数据移动到了另一个实例中
    Redis 会返回一个 ASK 错误信息,包含了该数据所在新实例的信息。当客户端对新实例发送 ASKING 命令来询问是否允许客户端进行后续的命令执行,允许则使用 GET 命令来获取数据。但 ASK 不会给客户端提供槽道的新对应关系,如果后续客户端提交同样的请求,还是会被发到原来的实例。

# 主从同步

Redis 提供了读写分离的主从模式。即读操作从主库从库都可以接收;写操作先在主库执行,之后同步给从库。

当多个 Redis 实例启动后,使用 replicaofslaveof (Redis 5.0-) 命令形成主从库关系,之后数据库按三个阶段进行第一次同步。

  • 建立连接,协商同步
    从库在和主库建立连接后,发送 psync 请求到主库,这个命令包含 runID 和 offset 两个参数,来通知主库即将进行同步。其中 runID 是每个 Redis 实例启动时都会生成的随机 ID,用来唯一标识一个实例。offset 是偏移量,表示第几次复制。这里传输的值分别是 "?" 和 "-1".
    主库收到请求后返回 FULLSYNC 响应,携带主库 runID 和当前复制进度 offset。从库收到后会记录下这两个参数。