Redis 集群

发布于:2025-05-11 ⋅ 阅读:(16) ⋅ 点赞:(0)

集群基本介绍

广义的集群,只要是多个机器构成了分布式系统,都可以称为是一个“集群”(主从模式,哨兵模式)

狭义的集群,Redis 提供的集群模式,在这个模式下主要解决的是存储空间不足的问题(拓展存储空间)

引入多组 master/slave,每一组 master/slave 存储数据全集的一部分,从而构成一个更大的整体

——>  Redis 集群  Cluster

每一组 master/slave 都可以称为是一个 分片  sharding

如果全量数据进一步增加,只需要增加更多的分片即可

数据分片算法

1)哈希求余

借助 hash 函数(例如 MD5),把一个 key 映射到整数,再针对数组的长度求余,得到一个数组的下标

MD5 本身就是一个计算 hash 值的算法,针对一个字符串里面的内容进行一系列的数学转换,最后得到一个 整数(十六进制)

1)md5 计算结果是定长的

无论输入的字符串多长,最终算出的结果就是固定长度

2)md5 计算结果是分散的 [哈希函数]

两个原字符串哪怕只有一小部分不一样,算出的 md5 值也会差别很大

3)md5 计算结果是不可逆的 [加密]

根据原字符串,容易计算 md5 值;根据 md5 值,理论上还原不出原字符串

hash(key) % N => 0    这个 key 就要存储到 0 号分片中

后续查询 key 时,也是相同的算法

随着业务数据的增长,原先的分片不足以保存了,就需要“扩容”——>

引入新的分片,N 就改变了    hash(key) % N  就不一定等于 0 了

在扩容后,如果发现某个数据不应该在当前的分片中,就需要重新分配了(搬运数据)

可以看出,上述 20 个数据,只有 3 个不需要搬运,那如果是 20 亿个数据?

上述级别的扩容,开销极大往往是不能直接在生产环境上操作的,只能通过“替换”的方式来实现扩容的

2)一致性哈希算法

在 哈希求余 这种操作中,当前 key 的分片是 交替 的

在 一致性哈希 的设定下,改进成 连续出现

1.把 0 ~ 2*32 -1 映射到一个圆环上,数据按照顺时针增长

2.假设当前存在三个分片,把分片均匀地放到圆环上

3.根据 key 计算 hash 值,再根据 hash 值 进行分片

扩容后——>

只需要把 0号分片 上的这一段数据搬运到 3号分片 即可

其余的分片都是不变的,搬运成本明显降低,但是不同分片上的的数据量就不均匀了(数据倾斜)

3)哈希槽分区算法

Redis 真正采用的是 哈希槽分区算法(hash slots)

hash_slot = crc16(key) % 16384        16384 = 16 * 1024 = 2 *14

相当于是把整个哈希值,映射到 16384 个槽位上,也就是 [0, 16384]

假设有三个分片:

~0号分片:[0, 5461],共5462个槽位

~1号分片:[5462, 10923],共5462个槽位

~2号分片:[10924, 16384],共5460个槽位

虽然不是严格的“均匀”,但是差异非常小,此时三个分片上的数据就是比较均匀的

以上只是一种可能的分片方式,每个分片持有的槽位号,可以是连续的,也可以是不连续的

每个分片,会使用“位图”这样的数据结构表示当前持有多少槽位号

16384个bit位,用每一位0/1来区分是否持有每一槽号位

对于16384个bit位,约需要 2kb 的空间

扩容后——>

~0号分片:[0, 4095],共5462个槽位

~1号分片:[5462, 9557],共5462个槽位

~2号分片:[10924, 15019],共5462个槽位

~3号分片:[4096, 5461] + [9558, 10923] + [15019, 16383],共5462个槽位

在上述过程中,只有被移动的槽位,对应的数据才需要搬运

针对某分片上的槽位号,不一定非得是连续的区间

Redis 中,当前某个分片包含的那些槽位都是可以手动配置的

1)Redis 集群中最多可以有 16384个分片吗?

如果每个分片只有一个槽位,对于集群的数据均匀是难以保证的

因此,Redis 的作者建议集群分片数不应该超过1000(可用性)

2)为什么是16384个槽位?

hehzuozzuozuzhttps://github.com/antirez/redis/issues/2576

 节点之间通过心跳包通信. 心跳包中包含了该节点持有哪些 slots. 这个是使用位图这样的数据结构表示的. 表示 16384 (16k) 个 slots, 需要的位图大小是 2KB. 如果给定的 slots 数更多了, 比如 65536个了, 此时就需要消耗更多的空间, 8 KB 位图表示了. 8 KB, 对于内存来说不算什么, 但是在频繁的网络心跳包中, 还是一个不小的开销的.
•  另⼀方面, Redis 集群一般不建议超过 1000 个分片. 所以 16k 对于最大 1000 个分片来说是足够用的, 同时也会使对应的槽位配置位图体积不至于很大

搭建集群环境

1)创建目录和配置

  在 generate.sh 中写入 shell 脚本

执行脚本后生成的目录(蓝色):

cluster-enabled yes:开启集群
cluster-config-file nodes.conf:集群节点生成的配置
cluster-node-timeout 5000:节点失联的超超时信息
cluster-announce-ip 172.30.0.101:节点自身IP(静态IP)
该 Redis 节点所在的主机的IP(当前是使用 docker 容器模拟的主机,此处写的是 docker 容器的IP)
Redis 节点自身绑定的端口(容器内端口),不同的容器内部可以有相同的接口
后续进行端口映射,再把这些容器内的端口映射到容器外的不同端口
cluster-announce-port 6379:节点自身的业务端口
cluster-announce-bus-port 16379:节点自身的总线端口(集群管理的信息交互是通过这个端口进行的)

业务端口:进行业务通信,响应 Redis 客户端的请求

管理端口:完成一些管理上的任务来进行通信(某个分片中从节点的晋升)

一个服务器,可以绑定多个端口

2)编写 docker-compose.yml

将下面的配置添加到 docker-compose.yml 中(注意缩进)

version: '3.7'
networks:
 mynet:
 ipam:
 config:
 - subnet: 172.30.0.0/24
services:
 redis1:
 image: 'redis:5.0.9'
 container_name: redis1
 restart: always
 volumes:
 - ./redis1/:/etc/redis/
 ports:
 - 6371:6379
 - 16371:16379
 command:
 redis-server /etc/redis/redis.conf
 networks:
 mynet:
 ipv4_address: 172.30.0.101
 redis2:
 image: 'redis:5.0.9'
 container_name: redis2
 restart: always
 volumes:
 - ./redis2/:/etc/redis/
 ports:
 - 6372:6379
 - 16372:16379
 command:
 redis-server /etc/redis/redis.conf
 networks:
 mynet:
 ipv4_address: 172.30.0.102
 redis3:
 image: 'redis:5.0.9'
 container_name: redis3
 restart: always
 volumes:
 - ./redis3/:/etc/redis/
 ports:
 - 6373:6379
 - 16373:16379
 command:
 redis-server /etc/redis/redis.conf
 networks:
 mynet:
 ipv4_address: 172.30.0.103
 redis4:
 image: 'redis:5.0.9'
 container_name: redis4
 restart: always
 volumes:
 - ./redis4/:/etc/redis/
 ports:
 - 6374:6379
 - 16374:16379
 command:
 redis-server /etc/redis/redis.conf
 networks:
 mynet:
 ipv4_address: 172.30.0.104
 redis5:
 image: 'redis:5.0.9'
 container_name: redis5
 restart: always
 volumes:
 - ./redis5/:/etc/redis/
 ports:
 - 6375:6379
 - 16375:16379
 command:
 redis-server /etc/redis/redis.conf
 networks:
 mynet:
 ipv4_address: 172.30.0.105
 redis6:
 image: 'redis:5.0.9'
 container_name: redis6
 restart: always
 volumes:
 - ./redis6/:/etc/redis/
 ports:
 - 6376:6379
 - 16376:16379
 command:
 redis-server /etc/redis/redis.conf
 networks:
 mynet:
 ipv4_address: 172.30.0.106
 redis7:
 image: 'redis:5.0.9'
 container_name: redis7
 restart: always
 volumes:
 - ./redis7/:/etc/redis/
 ports:
 - 6377:6379
 - 16377:16379
 command:
 redis-server /etc/redis/redis.conf
 networks:
 mynet:
 ipv4_address: 172.30.0.107
 redis8:
 image: 'redis:5.0.9'
 container_name: redis8
 restart: always
 volumes:
 - ./redis8/:/etc/redis/
 ports:
 - 6378:6379
 - 16378:16379
 command:
 redis-server /etc/redis/redis.conf
 networks:
 mynet:
 ipv4_address: 172.30.0.108
 redis9:
 image: 'redis:5.0.9'
 container_name: redis9
 restart: always
 volumes:
 - ./redis9/:/etc/redis/
 ports:
 - 6379:6379
 - 16379:16379
 command:
 redis-server /etc/redis/redis.conf
 networks:
 mynet:
 ipv4_address: 172.30.0.109
 
 redis10:
 image: 'redis:5.0.9'
 container_name: redis10
 restart: always
 volumes:
 - ./redis10/:/etc/redis/
 ports:
 - 6380:6379
 - 16380:16379
 command:
 redis-server /etc/redis/redis.conf
 networks:
 mynet:
 ipv4_address: 172.30.0.110
 redis11:
 image: 'redis:5.0.9'
 container_name: redis11
 restart: always
 volumes:
 - ./redis11/:/etc/redis/
 ports:
 - 6381:6379
 - 16381:16379
 command:
 redis-server /etc/redis/redis.conf
 networks:
 mynet:
 ipv4_address: 172.30.0.111

此处为了后续创建静态 IP,需要先手动创建出网络,同时给这个网络也分配 IP

网络原理-IP 协议-IP 地址规则

IP 地址 = 网络号 + 主机号

使用子网掩码区分网络号和主机号

ipv4_address:此处也可以不进行端口映射

映射的目的是为了在容器外面通过客户端直接进行访问

此处配置静态IP

网络号要和之前的网段一致,主机号可以随意配置

3)启动容器

docker-compose up -d

在启动容器前,要确保之前的 redis 都被干掉

ps aux | grep redis

查看 Redis 相关进程

docker ps -a

查看 容器

4)构建集群

在命令行中执行以下命令

redis-cli --cluster create 172.30.0.101:6379 172.30.0.102:6379
172.30.0.103:6379 172.30.0.104:6379 172.30.0.105:6379 172.30.0.106:6379
172.30.0.107:6379 172.30.0.108:6379 172.30.0.109:6379 --cluster-replicas 2

--cluster create:表示建立集群,后面填写每个节点的 IP 和 地址

--cluster-replicas 2:表示每个主节点需要两个从节点备份

从 101 ~ 109 九个节点现在是一个整体,使用客户端上的任意一个节点,本质上是等价的

连接 Redis 客户端:

redis-cli -h 172.30.0.101 -p 6379  静态 IP

redis-cli -p 6379  端口映射

cluster nodes  查看当前集群的信息

设置成集群模式后,当前数据就分片了

key1 通过 hash 计算后,属于 102 这个分片,在当前分片 101 上不能设置

可以在启动 redis-cli 时,加上 -c 选项

此时客户端发现当前 key 不再当前分片上,会自动重定向到对应的分片主机上

使用集群后,之前学的命令大多都可以正常使用(可以使用 hash tag ..处理特殊情况)

故障处理

docker stop redis    停止节点

docker start redis    启动节点

在集群中,从节点挂了,影响不大;主节点挂了,会自动把该主节点下的从节点,挑选一个晋升成主节点(类似于 哨兵)

集群机制,也能处理故障转移,但是此处的处理流程和 哨兵 不太一样

1)故障判定

集群中的所有节点, 都会周期性的使用心跳包进行通信.
1. 节点 A 给 节点 B 发送 ping 包, B 就会给 A 返回⼀个 pong 包. ping 和 pong 除了 message type
属性之外, 其他部分都是一样的. 这里包含了集群的配置信息(该节点的id, 该节点从属于哪个分片,
是主节点还是从节点, 从属于谁, 持有哪些 slots 的位图...)
2. 每个节点, 每秒钟, 都会给一些随机的节点发起 ping 包, ⽽不是全发⼀遍. 这样设定是为了避免在节点很多的时候, 心跳包也非常多(比如有 9 个节点, 如果全发, 就是 9 * 8 有 72 组心跳了, 而且这是按照 N^2 这样的级别增长的)
3. 当节点 A 给节点 B 发起 ping 包, B 不能如期回应的时候, 此时 A 就会尝试重置和 B 的 tcp 连接, 看能否连接成功. 如果仍然连接失败, A 就会把 B 设为 PFAIL 状态(相当于主观下线).
4. A 判定 B 为 PFAIL 之后, 会通过 redis 内置的 Gossip 协议, 和其他节点进行沟通, 向其他节点确认 B的状态. (每个节点都会维护一个自己的 "下线列表", 由于视角不同, 每个节点的下线列表也不一定相同)
5. 此时 A 发现其他很多节点, 也认为 B 为 PFAIL, 并且数目超过总集群个数的一半, 那么 A 就会把 B 标记成 FAIL (相当于客观下线), 并且把这个消息同步给其他节点(其他节点收到之后, 也会把 B 标记成FAIL)至此, B 就彻底被判定为故障节点了
核心原则是保证每个 slots 都能正常工作(存取数据)

2)故障转移

上述例子中, B 故障, 并且 A 把 B FAIL 的消息告知集群中的其他节点.

 如果 B 是从节点, 那么不需要进行故障迁移.
•  如果 B 是主节点, 那么就会由 B 的从节点 (比如 C 和 D) 触发故障迁移了
所谓故障迁移, 就是指把从节点提拔成主节点, 继续给整个 redis 集群提供支持
具体流程如下:
1. 从节点判定自己是否具有参选资格. 如果从节点和主节点已经太久没通信(此时认为从节点的数据和主节点差异太大了), 时间超过阈值, 就失去竞选资格
2. 具有资格的节点, 比如 C 和 D, 就会先休眠一定时间. 休眠时间 = 500ms 基础时间 + [0, 500ms] 随机时间 + 排名 * 1000ms. offset 的值越大, 则排名越靠前(越小)
3. 比如 C 的休眠时间到了, C 就会给其他所有集群中的节点, 进行拉票操作. 但是只有主节点才有投票资格
4. 主节点就会把自己的票投给 C (每个主节点只有 1 票). 当 C 收到的票数超过主节点数目的一半, C 就会晋升成主节点. (C 自己负责执行 slaveof no one, 并且让 D 执行 slaveof C).
5. 同时, C 还会把自己成为主节点的消息, 同步给其他集群的节点. 大家也都会更新自己保存的集群结构信息
上述选举的过程, 称为 Raft 算法, 是一种在分布式系统中广泛使用的算法
在随机休眠时间的加持下, 基本上就是谁先唤醒, 谁就能竞选成功

3)集群宕机

某个或者某些节点宕机, 有的时候会引起整个集群都宕机 (称为 fail 状态)
以下三种情况会出现集群宕机:
某个分片, 所有的主节点和从节点都挂了.
某个分片, 主节点挂了, 但是没有从节点.
超过半数的 master 节点都挂了

集群扩容

当前的集群中,有 101 ~ 109,9个主机,形成 3主,6从的结构

现在把 110 和 111 也加入到集群中,110 为 master,111为 slave,数据分片 3 ——> 4

1)把新的节点加入到集群中

redis-cli --cluster add-node 172.30.0.110:6379 172.30.0.101:6379
add-node 后的第一组地址是新节点的地址,第二组地址是集群中的任意节点的地址

2)重新分配 slots

redis-cli --cluster reshard 172.30.0.101:6379
reshard 后的地址是集群中的任意节点的地址
执行之后, 会进入交互式操作, redis 会提示用户输入以下内容:
多少个 slots 要进行 reshard ? (此处填写 4096)
哪个节点来接收这些 slots ? (此处填写 172.30.0.110 这个节点的集群节点 id)
这些 slots 从哪些节点搬运过来? (此处填写 all)
all:表示从其他每个持有 slots 的 master 都拿过来点
手动指定:从某个或者某几个节点来移动 slots(以 done 结尾)
输入 all 之后,会给出搬运计划(还没有真正搬运),当输入 yes 之后,搬运真正开始 ——>
重新划分 slots,把 slots 对应上的数据也搬运到新主机上(比较重量级的操作)
在搬运 slots / key 的过程中,客户端能否访问 Redis 集群?
大部分的 key 不需要搬运,针对这些未搬运的 key,此时可以正常访问
针对正在搬运的 key,是有可能出现访问出错的情况

追求更高的可用性,减少对用户的影响 ——>

搞一组新的机器,重新搭建集群,并且把数据导入进来,使用新的集群替代旧的集群(成本最高)

3)给新的主节点添加从节点

redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 --cluster
slave --cluster-master-id [172.30.1.110 节点的 nodeId]
集群的缩容
1)删除从节点
redis-cli --cluster del-node [ 集群中任一节点 ip:port] [ 要删除的从机节点 nodeId]
2)重新分配 slots
redis-cli --cluster reshard 172.30.0.101:6379
3)删除主节点
redis-cli --cluster del-node [ 集群中任一节点 ip:port] [ 要删除的从机节点 nodeId]

网站公告

今日签到

点亮在社区的每一天
去签到