Redis(三):Redis如何实现高可扩展?

597 阅读7分钟

Redis集群提高了Redis的可扩展性,Redis的集群方案主要有Redis Cluster和Codis。

Redis Cluster

Redis Cluster是Redis官方提供的去中心化集群方案,通过分片来进行数据共享,并提供复制和故障转移功能。

Redis Cluster如何分片?

Redis集群通过分片的方式来保存数据库中的键值对。集群的整个数据库被分为16384个槽,数据库中的每个键都属于这16384个槽的其中一个,集群中的每个节点可以负责0~16384个槽。

当数据库中的16384个槽都有节点在处理时,集群处于上线状态;如果有任何一个槽没有节点处理时,集群处于下线状态。

槽指派命令

通过向节点发送cluster addslots命令可以将一个或多个槽指派给该节点。

如何维护槽信息?

每个节点的clusterNode结构slots属性和numslot属性记录节点负责的槽信息。

clusterNode.slots是一个二进制数组,长度为16384/8=2048字节,共包含16384个二进制位。Redis对clusterNode.slots数组中的16384个二进制位进行编号,根据索引i上的二进制位值来判断节点负责哪些槽(二进制位值=1,表示节点负责处理槽i)。 clusterNode.numslot记录该节点负责的槽数量。

在这里插入图片描述

节点之间如何共享槽指派信息?

集群中的每个节点都知道16384个槽都分别被指派给了集群中的哪些节点。

每个节点会将自己的clusterNode.slots数组发送给集群中的其他节点,告知其他节点自己负责处理哪些槽。收到槽指派信息的节点会在自己的clusterState.nodes字典中查找该节点对应的clusterNode结构,并对该clusterNode.slots数组进行更新。

但是只是将槽指派信息保存在每个节点的clusterNode.slots数组中,想要知道某个槽是否被指派或者指派给了那个节点,都需要遍历clusterState.nodes字典中的所有clusterNode.nodes数组,时间复杂度为O(n)。

所以每个节点clusterState结构会使用一个slots数组会记录集群中16384个槽的指派信息,每个数组项指向一个clusterNode结构,表示槽i由该节点负责。想要知道某个槽的指派信息时,只需要访问clusterState.slots[i]即可,时间复杂度为O(1)。 在这里插入图片描述

集群中执行命令的过程

客户端向节点发送命令,接收命令的节点会计算出命令要处理的key属于哪个槽,并检查这个槽是否由自己负责,如果这个槽不是由自己负责,则向客户端返回一个MOVED错误,指引客户端请求正确的节点,并再次发送命令。

1. 如何计算键属于哪个槽 计算key的哈希值,并将哈希值对16384个槽取模,即可得到key所在的槽位。

2. 如何判断槽是否由自己负责呢? 根据槽位检查自己的clusterState.slots[i]是否等于clusterState.myself,如果是,则表示该槽位由自己负责,如果不是,根据clusterState.slots[i]指向的clusterNode记录的节点IP地址和端口号指引客户端向处理该槽的节点发送命令请求。

3. MOVED错误 MOVED错误会向客户端返回正确负责该key的槽、负责处理该key的节点IP地址、端口号,客户端通过MOVED错误返回的IP地址、端口号连接点点并重新发送命令请求。

在这里插入图片描述

重新分片

Redis集群的重新分片操作可以将任意数量已经指派给某个节点的槽改为指派给另一个节点,并且移动相关槽位负责的键值对。

重新分片过程

重新分片操作由Redis集群管理软件redis-trib负责执行,redis-trib通过向源节点和目标节点发送命令来进行重新分片操作。

Redis 重新分片的单位是槽,一个槽一个槽进行迁移,当一个槽正在迁移时,这个槽就处于中间过渡状态。这个槽在原节点的状态为migrating,在目标节点的状态为importing,表示数据正在从源流向目标。

  1. redis-trib 首先会在源和目标节点设置好中间过渡状态;
  2. 然后一次性获取源节点槽位的所有key列表(keysinslot指令,可以部分获取),再挨个key进行迁移,每个key的迁移过程是以原节点作为目标节点的客户端: (1) 原节点对当前的key执行dump指令得到序列化内容; (2) 源节点向目标节点发送指令restore携带序列化的内容作为参数; (3) 目标节点进行反序列化将内容恢复到目标节点的内存中,然后返回源节点OK; (4) 原节点收到后再把当前节点的key删除;

(1) ~ (4) 执行期间主线程阻塞,如果key的内容过大会导致源节点和目标节点卡顿,影响集群稳定性,所以需要尽可能避免大key产生。

在这里插入图片描述

ASK错误

在进行重新分片期间,可能出现被迁移槽的一部分键值对在源节点,一部分键值对在目标节点。当客户端请求的键属于被迁移的槽时:

  1. 源节点在自己的数据库中查找指定的键,如果找到,则直接执行命令;
  2. 如果源节点中找不到指定的键,则说明该键不存在或已经被迁移到了目标节点,源节点将向客户端返回一个ASK错误(槽、节点IP地址、端口号);
  3. 客户端收到回复的ASK错误,将向返回的节点IP地址、端口号发送一个不带参数的asking命令(槽迁移没完成前,目标节点还不认为该节点由自己负责,会重定向回源节点,形成重定向循环,所以需要向目标节点发送一个acking指令,告知目标节点需要处理下一条指令);
  4. 客户端请求目标节点,并再次发送命令;

Codis

Codis是一个国内开源的代理中间件,当客户端向Codis发送命令时,Codis负责将命令转发到Redis服务器来执行,并将结果再返回给客户端。

Codis上挂接的所有Redis服务器构成一个集群,当集群空间不足时,可以通过动态增加Redis服务器来实现扩容需求。

在这里插入图片描述

Codis是无状态的,只是一个转发代理中间件,可以启动多个Codis实例供客户端使用,每个Codis节点都是对等的。

Codis如何分片?

Codis将默认划分1024个槽,每个key都属于这1024个槽的其中一个。当客户端发送命令时,首先计算key的哈希值,再将哈希值对1024取模,即可得到key对应的槽位。

Codis会在内存维护槽位和Redis实例的映射关系,每个槽位都会唯一映射到一个Redis实例。

槽位同步

Codis会使用一个分布式配置存储数据库专门用来持久化槽位关系,如ZooKeeper。

Codis将槽位关系存储在ZooKeeper,并且提供了一个DashbBoard来观察和修改槽位关系,当槽位发生变化时,CodisProxy会监听到变化并重新同步槽位关系。

在这里插入图片描述

扩容

Codis增加了slotsscan命令,可以遍历某个槽下所有的key。Codis通过slotsscan命令获取待迁移槽位的所有key,然后挨个迁移每个key到新的Redis节点。

但是Codis无法判断迁移中的key存在哪个实例。当Codis接收到正在迁移槽位中的key后,会立即强制对当前的单个key进行迁移,迁移完成后再将请求转发到新的Redis节点。

比较Redis Cluster和Codis

  1. 分布式事务:
  • Codis是代理中间件,所有的key都分散在不同的Redis实例中,无法实现分布式事务;
  • Redis Cluster支持分布式事务;
  1. 槽位划分:
  • Redis Cluster划分16384个槽位;
  • Codis默认划分1024个槽位;
  1. 槽位迁移:
  • Redis Cluster每个节点都保存槽位关系,通过发送消息来通知槽位变更;
  • Codis将槽位关系维护在ZooKeeper,当Codis监听到槽位变更则重新同步;
  1. 目标节点定位:
  • Codis需要经过Proxy来定位目标节点;
  • Redis Cluster可以直接定位目标节点;