相关文章:
在搭建 Redis 集群之前,我们先来了解下相关的概念
-
集群
- 将多台服务器连接起来共同工作,在某种程度上,可以被看作是一台服务器,集群中单台服务器通常被称为节点
-
负载均衡
- 将一台服务器上要处理的请求,根据负载均衡算法分配到其他服务器上去处理,从而减少该台服务器的负载,防止因为负载过大而造成响应超时或宕机等意外情况的发生
-
分布式
-
将不同的业务模块部署在不同的服务器上
-
和集群的区别在于
-
集群是将同一个业务部署在多台服务器上
-
分布式是将不同的业务部署在多台服务器上
-
-
一、Redis 集群介绍
-
Redis 从 3.0 版本开始支持集群,其是一个提供多个 Redis 节点间共享数据的程序集
-
Redis 集群通过分区来提供一定程度的可用性,体现在实际环境中,当某些节点出现故障时,仍然能继续处理命令;但时如果发生较大的故障 (如:大多数主服务器不可用),集群将停止运行
-
Redis 集群的优势
-
自动切分数据到不同的节点上
-
当整个集群的部分节点出现故障或无法与其他节点通信时,仍然能继续处理命令
-
-
Redis 集群是完全去中心化的,不存在中心节点或代理节点
-
Redis 集群没有统一的入口,当客户端连接集群时,只需要连接集群中的任意节点即可,集群内部的节点是相互通信的 (ping-pong 机制),每个节点都是一个 Redis 实例
二、Redis 集群 TCP 端口
-
每个 Reids 集群需要建立两个 TCP 连接,监听两个端口
-
客户端端口
-
通常是 6379,用于与客户端通信,需要对所有客户端和集群节点开放
-
集群节点需要通过该端口向客户端转义数据
-
-
集群总线端口
-
通常是 6379 + 10000,用于节点之间通过二进制协议进行通信,只需要对集群节点开发
-
集群中各节点通过集群总线进行检测故障节点、更新配置等操作
-
-
三、Redis 集群数据分片
-
Redis 集群没有使用一致性 Hash 算法, 而是引入了哈希槽的概念
-
Redis 集群有 16384 个哈希槽,每个 key 通过 CRC16 校验后再对 16384 进行取模来决定放入哪个槽中
-
Redis 集群中每个节点负责一部分哈希槽,举例说明,假如当前集群有 3 个节点 A、B、C,那么
-
节点 A 的哈希槽范围为:
[0-5500]
-
节点 B 的哈希槽范围为:
[5501-11000]
-
节点 C 的哈希槽范围为:
[11001-16384]
-
-
通过哈希槽的结构,我们可以很轻松地添加或删除集群中的节点
-
如果要添加节点 D,只需要将节点 A、B、C 的部分哈希槽移动到节点 D 即可
-
如果要删除节点 A,只需要将节点 A 的哈希槽移动到节点 B、C,然后将节点 A 从集群中移除即可
-
由于将哈希槽从一个节点移动到另一个节点不需要停止操作,所以无论添加删除或改变某个节点哈希槽的数量,都不会造成集群无法使用
-
四、Redis 集群主从模型
-
为了当部分节点失败或大部分节点无法通信的情况下集群仍然可用,集群使用了主从模型,每个哈希槽具有 1 到 N 个副本
-
在上面提到的例子中 (具有 A、B、C 3 个节点的集群),假如节点 B 失败,那么集群将会因为缺少
[5501-11000]
范围的哈希槽而不可用 -
但是如果我们在创建集群时,为 Master A、B、C 分别添加一个 Slave A1、B1、C1,即创建了一个 3 主 3 从的集群,这样的话即使节点 B 失效,集群也会选举节点 B1 作为新的 Master 来提供服务,那么集群便不会因为缺少
[5501-11000]
范围的哈希槽而不可用 -
但需要注意的是,如果节点 B 和 B1 都失败了,集群将会因为缺少
[5501-11000]
范围的哈希槽而不可用
五、Redis 集群一致性保证
-
Redis 集群无法保证数据的强一致性,在以下情况下,Redis 集群可能会丢失客户端的写入操作
-
使用了异步复制
-
所谓异步复制,是指主从节点之间采用异步的方式来同步数据
-
流程如下
-
客户端向 Master B 发起写操作
-
Master B 回应客户端写操作成功
-
Master B 向它的 Slave B1、B2、B3 同步该写操作
-
-
当 Master B 回应客户端写操作成功时,并不会等待 Slave B1、B2、B3 确认后再回复,因为这会对 Redis 的性能造成影响
-
如果 Master B 在回应客户端写操作成功后,在发送写操作到 Slave B1、B2、B3 前发生故障宕机,随后在 Slave B1、B2、B3 (未收到写操作) 中会选举出一个新的 Master,那么之前的写操作就永远丢失了
-
因此我们需要根据实际的业务需求,在性能和一致性之间进行权衡
-
-
在网络分区期间,客户端与少数实例 (至少包括一个 Master) 隔离
-
举例说明
-
假如当前集群有 6 个节点 A、B、C、A1、B1、C1,其中 A、B、C 为 Master,A1、B1、C1 为 Slave,另外还有个一个客户端 Z1
-
在发生网络分区后,可能在分区的一侧有节点 A,C,A1,B1,C1,分区的另一侧有节点 B、Z1
-
此时 Z1 仍然能向 Master B 发起写操作,如果网络分区时间很短,当分区恢复后,集群可以继续正常工作;如果网络分区时间很长,在另一边会认为 Master B 已经挂了,于是选举节点 B1 作为新的 Master,当分区恢复后,由于节点 B1 变为了 Master,节点 B 则会变成节点 B1 的 Slave,节点 B 会清除自身的数据去同步节点 B1 的数据,因此会丢失之前 Z1 向节点 B 发起的写操作
-
-
这就是所谓的 Redis 集群脑裂,可以通过设置以下两个参数来尽量避免数据的丢失
-
min-replicas-to-write
- 表示 Slave 连接到 Master 的最小数量,默认为 0 (0 表示禁用)
-
min-replicas-max-lag
- 表示 Slave 连接到 Master 的最大延迟时间,默认为 10 s
-
假如
min-replicas-to-write
为 3,min-replicas-max-lag
为 10,则表示一个 Master 至少需要 3 个 Slave,且这 3 个 Slave 连接到 Master 的时间不能超过 10 s,否则 Master 将停止写入
-
-
-
六、Redis 集群配置参数
-
cluster-enabled <yes/no>
- 集群开关,默认为 false
-
cluster-config-file <filename>
- 节点集群配置文件,该文件由节点自身维护
-
cluster-node-timeout <milliseconds>
- 访问节点超时时间,默认为 15000 ms
-
cluster-replica-validity-factor <factor>
-
在进行故障转移时,Slave 会申请成为 Master,但是有些 Slave 可能与 Master 断开连接的时间过长,导致数据过于陈旧,这样的 Slave 不应该被提升为 Master,该参数主要用于判断 Slave 和 Master 断线的时间是否过长
-
如果为 0,则表示集群始终会进行主从切换,而不管主从节点之间断开的时间长短
-
如果为正数,则比较断开的时间和
(cluster-node-timeout * cluster-replica-validity-factor) + repl-ping-replica-period
的大小,假设node-timeout
为 30 s、replica-validity-factor
为 10 s、repl-ping-replica-period
为 10 s,那么如果断开时间超过 310 s,Slave 将不会尝试进行故障转移
-
-
cluster-migration-barrier <count>
- 当 Master 拥有的 Slave 数量大于该值时,Slave 才能迁移到其他的 Master 上,默认为 1
-
cluster-require-full-coverage <yes/no>
- 默认情况下,集群中的所有哈希槽都有节点负责,集群才能正常运行,默认为 yes;当设置为 no 时,可以在哈希槽没有完全分配的情况下正常运行
七、搭建 Redis 集群
-
为了实现 Redis 集群的高可用,我们至少需要 6 个节点 (3 主 3 从),这里我们在一台服务器上运行 6 个 Redis 实例,来搭建一个伪集群,修改端口号为 (6380 - 6385)
-
搭建流程如下
-
1、在服务器上搭建一个单机版的 Redis,具体流程参考 --> Redis – 02 – Linux上源码包安装Redis
-
2、在
/usr/local/
目录下新建redis-cluster
目录,用于存放集群节点-
cd /usr/local/
-
mkdir redis-cluster
-
-
3、将第 1 步安装完的 Redis 的 bin 目录下的所有文件复制到
/usr/local/redis-cluster/redis01/
目录下- cp -r /usr/local/redis/bin/ /usr/local/redis-cluster/redis01/
-
4、修改
/usr/local/redis-cluster/redis01/
目录下的 redis.conf 配置文件-
vim /usr/local/redis-cluster/redis01/redis.conf
# 将 bind 127.0.0.1 一行注释掉 # bind 127.0.0.1 # 设置端口 port 6380 # 开启守护进程 daemonize yes # 设置 pid 文件 pidfile /var/run/redis-6380.pid # 设置 log 文件 logfile "./redis-6380.log" # 设置工作目录 dir ./ # 设置 Master 密码 masterauth 123456 # 设置密码 requirepass 123456 # 开启集群模式 cluster-enabled yes # 设置节点集群配置文件 cluster-config-file nodes-6380.conf # 设置访问节点超时时间 cluster-node-timeout 15000
-
:wq
-
-
5、在
/usr/local/redis-cluster/redis01/
目录下编写启动脚本-
vim /usr/local/redis-cluster/redis01/start.sh
#!/bin/bash #实例路径 APP_NAME=/usr/local/redis-cluster/redis01/redis-server #配置文件路径 APP_CONF=/usr/local/redis-cluster/redis01/redis.conf #使用说明,用来提示输入参数 usage() { echo "Usage: run.sh {start|stop|restart|status}" exit 1 } #检查程序是否正在运行 is_running() { pid=`ps -ef|grep $APP_NAME|grep -v grep|awk '{print $2}'` #如果不存在则返回1,存在则返回0 if [ -z "${pid}" ]; then return 1 else return 0 fi } #启动 start() { is_running if [ $? -eq 0 ]; then echo "${APP_NAME} is already running, pid=${pid}" else ${APP_NAME} ${APP_CONF} fi } #停止 stop() { is_running if [ $? -eq 0 ]; then kill -9 $pid else echo "${APP_NAME} is not running" fi } #查看运行状态 status() { is_running if [ $? -eq 0 ]; then echo "${APP_NAME} is running, pid is ${pid}" else echo "${APP_NAME} is not running" fi } #重启 restart() { stop sleep 5 start } #根据输入的参数,选择对应的执行方法,不输入则执行使用说明 case "$1" in start) start ;; stop) stop ;; status) status ;; restart) restart ;; *) usage ;; esac
-
:wq
-
chmod +x /usr/local/redis-cluster/redis01/start.sh
-
-
6、将
/usr/local/redis-cluster/redis01/
目录下的所有文件复制 5 份到/usr/local/redis-cluster/(redis02 - redis06)/
目录下-
cp -r /usr/local/redis-cluster/redis01/ /usr/local/redis-cluster/redis02/
-
cp -r /usr/local/redis-cluster/redis01/ /usr/local/redis-cluster/redis03/
-
cp -r /usr/local/redis-cluster/redis01/ /usr/local/redis-cluster/redis04/
-
cp -r /usr/local/redis-cluster/redis01/ /usr/local/redis-cluster/redis05/
-
cp -r /usr/local/redis-cluster/redis01/ /usr/local/redis-cluster/redis06/
-
在 redis02 - redis06 五个文件中,修改 redis.conf 配置文件和 start.sh 启动脚本
-
redis.conf 配置文件
-
将
port
分别改为6381 - 6385
-
将
pidfile
分别改为/var/run/redis-(6381 - 6385).pid
; -
将
logfile
分别改为./redis-(6381 - 6385).log
-
将
cluster-config-file
分别改为nodes-(6381 - 6385).conf
-
-
start.sh 启动脚本
- 将
redis01
分别改为redis02 - redis06
- 将
-
-
-
7、在
/usr/local/redis-cluster/
目录下编写总的启动脚本和总的关闭脚本-
vim /usr/local/redis-cluster/start.sh
#!/bin/bash #项目路径 REDIS01_PATH=/usr/local/redis-cluster/redis01 REDIS02_PATH=/usr/local/redis-cluster/redis02 REDIS03_PATH=/usr/local/redis-cluster/redis03 REDIS04_PATH=/usr/local/redis-cluster/redis04 REDIS05_PATH=/usr/local/redis-cluster/redis05 REDIS06_PATH=/usr/local/redis-cluster/redis06 #使用说明,用来提示输入参数 usage() { echo "Usage: start.sh {all|redis01|redis02|redis03|redis04|redis05|redis06}" exit 1 } #启动所有 all() { cd ${REDIS01_PATH} ./start.sh start cd ${REDIS02_PATH} ./start.sh start cd ${REDIS03_PATH} ./start.sh start cd ${REDIS04_PATH} ./start.sh start cd ${REDIS05_PATH} ./start.sh start cd ${REDIS06_PATH} ./start.sh start } #启动redis01 redis01() { cd ${REDIS01_PATH} ./start.sh start } #启动redis02 redis02() { cd ${REDIS02_PATH} ./start.sh start } #启动redis03 redis03() { cd ${REDIS03_PATH} ./start.sh start } #启动redis04 redis04() { cd ${REDIS04_PATH} ./start.sh start } #启动redis05 redis05() { cd ${REDIS05_PATH} ./start.sh start } #启动redis06 redis06() { cd ${REDIS06_PATH} ./start.sh start } #根据输入的参数,选择对应的执行方法,不输入则执行使用说明 case "$1" in all) all ;; redis01) redis01 ;; redis02) redis02 ;; redis03) redis03 ;; redis04) redis04 ;; redis05) redis05 ;; redis06) redis06 ;; *) usage ;; esac
-
:wq
-
chmod +x /usr/local/redis-cluster/start.sh
-
vim /usr/local/redis-cluster/stop.sh
#!/bin/bash #项目路径 REDIS01_PATH=/usr/local/redis-cluster/redis01 REDIS02_PATH=/usr/local/redis-cluster/redis02 REDIS03_PATH=/usr/local/redis-cluster/redis03 REDIS04_PATH=/usr/local/redis-cluster/redis04 REDIS05_PATH=/usr/local/redis-cluster/redis05 REDIS06_PATH=/usr/local/redis-cluster/redis06 #使用说明,用来提示输入参数 usage() { echo "Usage: stop.sh {all|redis01|redis02|redis03|redis04|redis05|redis06}" exit 1 } #关闭所有 all() { cd ${REDIS01_PATH} ./start.sh stop cd ${REDIS02_PATH} ./start.sh stop cd ${REDIS03_PATH} ./start.sh stop cd ${REDIS04_PATH} ./start.sh stop cd ${REDIS05_PATH} ./start.sh stop cd ${REDIS06_PATH} ./start.sh stop } #关闭redis01 redis01() { cd ${REDIS01_PATH} ./start.sh stop } #关闭redis02 redis02() { cd ${REDIS02_PATH} ./start.sh stop } #关闭redis03 redis03() { cd ${REDIS03_PATH} ./start.sh stop } #关闭redis04 redis04() { cd ${REDIS04_PATH} ./start.sh stop } #关闭redis05 redis05() { cd ${REDIS05_PATH} ./start.sh stop } #关闭redis06 redis06() { cd ${REDIS06_PATH} ./start.sh stop } #根据输入的参数,选择对应的执行方法,不输入则执行使用说明 case "$1" in all) all ;; redis01) redis01 ;; redis02) redis02 ;; redis03) redis03 ;; redis04) redis04 ;; redis05) redis05 ;; redis06) redis06 ;; *) usage ;; esac
-
:wq
-
chmod +x /usr/local/redis-cluster/stop.sh
-
-
8、启动所有 Redis 实例
- /usr/local/redis-cluster/start.sh all
-
9、查看 Reids 实例进程
-
ps -ef | grep redis
-
-
10、将第 1 步安装完的 Redis 的 bin 目录下的 redis-cli 文件复制到
/usr/local/redis-cluster/
目录下- cp /usr/local/redis/bin/redis-cli /usr/local/redis-cluster/
-
11、创建 Redis 集群
-
redis-cli -a 123456 --cluster create 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 127.0.0.1:6385 --cluster-replicas 1
-
注意:如果使用的是外网 IP,且使用的是阿里云的服务器,记的在安全组中配置相关端口 (6380 - 6385)
-
在命令执行中途,有地方需要手动输入,输入 yes 即可
-
如上所示,展示了 Redis 集群为每个节点分配的哈希槽,总共有 6 个节点,其中 3 个为 Master,分别映射了
[0-5460]
、[5461-10922]
、[10923-16383]
区间的哈希槽;另外 3 个为 Slave,分配的哈希槽为 0
-
-
12、查看 Redis 集群信息
-
/usr/local/redis-cluster/redis-cli -a 123456 -p 6380
-
如上所示,端口为 6380、6381、6382 的 Redis 实例为 Master,其分别对应端口为 6384、6385、6383 的 Slave,主从比例为 1:1,即创建集群时附带的比例参数
--cluster-replicas 1
-
通过这种方式创建的主从对应关系是随机的
-
-
13、至此,Redis 集群就搭建成功了
-
四、Redis 集群测试
-
数据分配 + 主从同步测试
-
Master 测试
-
连接 Redis 集群 Master
-
/usr/local/redis-cluster/redis-cli -a 123456 -p 6380 -c
-
注意:此处一定要加上
-c
,否则节点之间无法自动跳转
-
-
进行测试
-
测试结果
-
由上可知,数据会被均匀地分配到不同的 Master 中
-
Redis 集群自动开启主从同步,Slave 会自动同步 Master 中的数据
-
-
-
Slave 测试
-
连接 Redis 集群 Slave
- /usr/local/redis-cluster/redis-cli -a 123456 -p 6385 -c
-
进行测试
-
测试结果
-
由测试结果可知,在端口为 6385 的 Slave 中写入数据,不会将数据保存在该 Slave 中,而是根据 key 对 16384 进行取模的值来决定放入哪个哈希槽中,此处放入的哈希槽为
[12539]
,位于端口为 6382 的 Master 中,也就是说,该数据会被保存到该 Master 中,并自动同步到端口为 6383 的 Slave 中 -
注意:Redis 集群通常做读写分离,也就是 Master 负责写操作,Slave 负责读操作,此处在 Slave 中进行写操作,只是为了测试起见
-
-
-
故障转移测试
-
为了方便测试,我们修改下 redis01 - redis06 的配置文件
# 将访问节点超时时间设为 5 s cluster-node-timeout 5000
-
关闭 Master,模拟 Master 宕机
- /usr/local/redis-cluster/stop.sh redis01
-
查看 Redis 日志
-
tail -f /usr/local/redis-cluster/redis05/redis-6384.log
-
-
查看 Redis 集群信息
-
/usr/local/redis-cluster/redis-cli -a 123456 -p 6384
-
如上所示,原来端口为 6380 的 Master,现在被标记为了 fail 状态,而原来端口为 6384 的 Slave 则变为了新的 Master,故障转移成功
-
-
开启 Master,模拟 Master 恢复
- /usr/local/redis-cluster/start.sh redis01
-
查看 Redis 日志
-
tail -f /usr/local/redis-cluster/redis01/redis-6380.log
-
-
查看 Redis 集群信息
-
tail -f /usr/local/redis-cluster/redis05/redis-6384.log
-
如上所示,原来端口为 6380 的 Master,现在已变为了端口为 6384 的 Master 的 Slave
-
-