Redis 集群
集群基本介绍
广义的集群,只要是多个机器构成了分布式系统,都可以称为是一个“集群”(主从模式,哨兵模式)
狭义的集群,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 脚本
执行脚本后生成的目录(蓝色):
该 Redis 节点所在的主机的IP(当前是使用 docker 容器模拟的主机,此处写的是 docker 容器的IP)Redis 节点自身绑定的端口(容器内端口),不同的容器内部可以有相同的接口后续进行端口映射,再把这些容器内的端口映射到容器外的不同端口
业务端口:进行业务通信,响应 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:6379172.30.0.103:6379 172.30.0.104:6379 172.30.0.105:6379 172.30.0.106:6379172.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)故障判定
核心原则是保证每个 slots 都能正常工作(存取数据)
2)故障转移
上述例子中, B 故障, 并且 A 把 B FAIL 的消息告知集群中的其他节点.
上述选举的过程, 称为 Raft 算法, 是一种在分布式系统中广泛使用的算法在随机休眠时间的加持下, 基本上就是谁先唤醒, 谁就能竞选成功
3)集群宕机
集群扩容
当前的集群中,有 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:6379add-node 后的第一组地址是新节点的地址,第二组地址是集群中的任意节点的地址
2)重新分配 slots
redis-cli --cluster reshard 172.30.0.101:6379reshard 后的地址是集群中的任意节点的地址
在搬运 slots / key 的过程中,客户端能否访问 Redis 集群?大部分的 key 不需要搬运,针对这些未搬运的 key,此时可以正常访问针对正在搬运的 key,是有可能出现访问出错的情况
追求更高的可用性,减少对用户的影响 ——>
搞一组新的机器,重新搭建集群,并且把数据导入进来,使用新的集群替代旧的集群(成本最高)
3)给新的主节点添加从节点
redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 --clusterslave --cluster-master-id [172.30.1.110 节点的 nodeId]
集群的缩容1)删除从节点redis-cli --cluster del-node [ 集群中任一节点 ip:port] [ 要删除的从机节点 nodeId]2)重新分配 slotsredis-cli --cluster reshard 172.30.0.101:63793)删除主节点redis-cli --cluster del-node [ 集群中任一节点 ip:port] [ 要删除的从机节点 nodeId]