宕机应对流程及关键问题探讨
- 将宕机的master下线
- 找一个slave作为master
- 通知所有的slave连接新的master
- 启动新的master和slave
- 可能会出现N台全量复制或者部分复制
- 谁来确认master宕机了,标准是什么?
- 找一个master,怎么找?谁来找?
修改配置后,原始的master恢复后,如何处理?
哨兵简介
哨兵(sentine)是一个分布式系统,用于对主从结构的每一个服务器进行监控,当出现故障时通过投票机制选择新的master并将所有slave连接到新的master上。
哨兵的作用
监控
- 检查master和slave是否正常运行
- master存活检测、master与slave运行情况检测
通知(提醒)
- 当被监控的服务器出现问题时,向其他(哨兵间、客户端)发送通知。
自动故障转移
- 断开master与slave的连接,选取一个slave作为新的master,并将其他slave连接到新的master上,并告知客户端新的服务器地址。
- 注意
- 哨兵也是一台redis服务器,只是不提供数据服务。
- 通常哨兵配置数量为单数,且数量不小于3。
环境配置,准备工作
使用vagrant搭建虚拟机
安装
cd D:\virtualmachine vagrant init centos/7 --box-version 2004.01 vagrant up
vagrant配置和命令使用请移步以下的文章了解
构建Redis哨兵模式
一主二从架构搭配三哨兵配置
- 主节点:127.0.0.1:6379
- 2个从节点:127.0.0.1:6380,127.0.0.1:6381
- 3个哨兵:127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381
开始安装
# 登录到虚拟机 vagrant ssh # 切换到root用户 sudo su root # 备份并更新yum源 mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo yum clean all yum makecache yum update -y # 安装依赖包 sudo yum install -y gcc make tcl vim wget epel-release # 安装python3 yum install -y https://repo.ius.io/ius-release-el7.rpm yum install -y python36 python36-pip ln -s /usr/bin/python3.6 /usr/bin/python3 ln -s /usr/bin/pip3.6 /usr/bin/pip3 # 创建redis用户和组 groupadd redis useradd -g redis -s /sbin/nologin -M redis # 创建redis安装目录 mkidr -p /usr/local/software cd /usr/local/software # 下载redis安装包 wget https://download.redis.io/releases/redis-7.2.4.tar.gz # 解压和安装redis tar -zxvf redis-7.2.4.tar.gz cd redis-7.2.4/ sudo make sudo make test sudo make install # 配置redis mkdir -p /usr/local/software/redis-7.2.4/data/redis/6379/ mkdir -p /usr/local/software/redis-7.2.4/data/redis/6380/ mkdir -p /usr/local/software/redis-7.2.4/data/redis/6381/ mkdir -p /usr/local/software/redis-7.2.4/data/sentinel/26379/ mkdir -p /usr/local/software/redis-7.2.4/data/sentinel/26380/ mkdir -p /usr/local/software/redis-7.2.4/data/sentinel/26381/ # 创建配置文件目录 mkdir -p /usr/local/software/redis-7.2.4/config # 创建文件 cd /usr/local/software/redis-7.2.4/config cat ../redis.conf | grep -v "#" | grep -v "^$" | tee redis-6379.conf > /dev/null # 配置redis-6379.conf vim redis-6379.conf ##################################redis-6379.conf#################################### pidfile "/usr/local/software/redis-7.2.4/data/redis/6379/redis_6379.pid" dir "/usr/local/software/redis-7.2.4/data/redis/6379/" ######################################################################################### # 配置redis-6380.conf cp redis-6379.conf redis-6380.conf sed -i 's/6379/6380/g' redis-6380.conf # 指定主从关系 vim redis-6380.conf replicaof 127.0.0.1 6379 # 配置redis-6381.conf cp redis-6379.conf redis-6381.conf sed -i 's/6379/6381/g' redis-6381.conf # 指定主从关系 vim redis-6380.conf replicaof 127.0.0.1 6379 # 配置sentinel-26379.conf cat ../sentinel.conf | grep -v "#" | grep -v "^$" | tee sentinel-26379.conf > /dev/null vim sentinel-26379.conf ##################################sentinel-26379.conf#################################### pidfile "/usr/local/software/redis-7.2.4/data/sentinel/26379/sentinel_26379.pid" dir "/usr/local/software/redis-7.2.4/data/sentinel/26379/" ######################################################################################### # 配置sentinel-26380.conf cp sentinel-26379.conf sentinel-26380.conf sed -i's/26379/26380/g' sentinel-26380.conf # 配置sentinel-26381.conf cp sentinel-26379.conf sentinel-26381.conf sed -i's/26379/26381/g' sentinel-26381.conf # 赋权 sudo chown -R redis:redis /usr/local/software/redis-7.2.4/ sudo chmod -R 775 /usr/local/software/redis-7.2.4/ # 为了优化 Redis 的运行环境,需要对 Linux 系统的内核参数进行调整,以下是具体的配置步骤 vim /etc/sysctl.conf ##################################/etc/sysctl.conf##################################### # vm.overcommit_memory 控制着系统的内存分配策略,其取值有 0、1、2 三种 # 取值为 0 时,内核会尝试智能判断可用内存和进程的内存需求,避免过度分配内存,但可能导致某些程序申请内存失败 # 取值为 1 时,内核允许内存过度分配,即进程可以申请超过系统实际可用物理内存和交换空间总和的内存 # Redis 在运行过程中可能会一次性分配大量内存,设置为 1 可避免 Redis 因内存分配失败而启动失败 vm.overcommit_memory = 1 # 设置 fs.file-max 参数为 65536 # fs.file-max 定义了系统中所有进程可以同时打开的最大文件描述符数量 # 文件描述符用于表示打开的文件、网络套接字等资源,Redis 作为高性能的键值对存储数据库,在处理大量客户端连接和文件操作时 # 可能会同时打开很多文件描述符,增大该值可以避免 Redis 出现“Too many open files”的错误 fs.file-max = 65536 # 设置 net.core.somaxconn 参数为 511 # net.core.somaxconn 是内核参数,用于设置每个网络套接字(socket)监听队列的最大长度 # 当有大量客户端向 Redis 发起连接请求时,这些请求会被放入监听队列等待处理 # 如果队列长度过小,可能会导致部分连接请求被拒绝,增大该值可以提高系统处理大量并发连接的能力 net.core.somaxconn = 511 ######################################################################################### # 注释说明接下来的操作是让刚才修改的内核参数立即生效 sysctl -p # 为了让 Redis 能够稳定且高效地运行,有时需要调整系统对用户或进程资源使用的限制。 vim /etc/security/limits.conf ##################################/etc/security/limits.conf############################## # 为 redis 用户设置硬限制的最大文件打开数为 65536。 # 硬限制是一个严格的上限,普通用户无法自行突破该限制,通常由系统管理员进行设置。 # Redis 在处理大量客户端连接、进行持久化操作等场景下,可能会同时打开大量的文件描述符。 # 将硬限制设置为 65536,可以确保 Redis 在高并发情况下有足够的文件描述符可用,避免因达到文件打开数限制而出现异常。 redis hard nofile 65536 # 为 redis 用户设置软限制的最大文件打开数为 65536。 # 软限制是一个建议性的上限,用户可以在不超过硬限制的情况下,通过特定命令自行调整软限制的值。 # 这里将软限制和硬限制设置为相同的值,意味着 redis 用户在正常使用过程中可以直接使用最大为 65536 的文件打开数,无需额外调整。 redis soft nofile 65536 ######################################################################################### # 为redis创建软链接 ln -s /usr/local/software/redis-7.2.4/bin/redis-server /usr/bin/redis-server ln -s /usr/local/software/redis-7.2.4/bin/redis-cli /usr/bin/redis-cli ln -s /usr/local/software/redis-7.2.4/bin/redis-sentinel /usr/bin/redis-sentinel # 启动redis sudo -u redis redis-server /usr/local/software/redis-7.2.4/config/redis-3679.conf sudo -u redis redis-server /usr/local/software/redis-7.2.4/config/redis-3680.conf sudo -u redis redis-server /usr/local/software/redis-7.2.4/config/redis-3681.conf
主从日志解析
########################################master日志解析################################################ # 3734 是 Redis 服务器进程的 ID,M 表示该进程是主节点(Master)。 # 02 Mar 2025 04:35:14.547 是日志记录的时间戳。 # 此日志表明 Redis 服务器已经完成初始化过程,内部基本结构和配置已设置好。 3734:M 02 Mar 2025 04:35:14.547 * Server initialized # 说明 Redis 正在加载 RDB(Redis Database)文件,该文件由 Redis 7.2.4 版本生成。 # RDB 是 Redis 用于持久化数据的一种快照方式。 3734:M 02 Mar 2025 04:35:14.547 * Loading RDB produced by version 7.2.4 # 表示加载的 RDB 文件从生成到现在已经过去了 40 秒,即该 RDB 文件的“年龄”为 40 秒。 3734:M 02 Mar 2025 04:35:14.547 * RDB age 40 seconds # 指出在创建这个 RDB 文件时,Redis 服务器在内存中占用的空间是 1.50 兆字节(Mb)。 3734:M 02 Mar 2025 04:35:14.547 * RDB memory usage when created 1.50 Mb # 表明 RDB 文件加载完成,从 RDB 文件中成功加载了 1 个键(key),且没有过期的键。 3734:M 02 Mar 2025 04:35:14.547 * Done loading RDB, keys loaded: 1, keys expired: 0. # 说明 Redis 从磁盘加载数据库(即 RDB 文件)所花费的时间为 0 秒(可能因精度问题显示为 0)。 3734:M 02 Mar 2025 04:35:14.547 * DB loaded from disk: 0.000 seconds # 意味着 Redis 服务器已准备好通过 TCP 协议接受客户端的连接请求。 3734:M 02 Mar 2025 04:35:14.547 * Ready to accept connections tcp # 表示 IP 地址为 127.0.0.1,端口号为 6380 的从节点(Replica)向主节点(当前 Redis 服务器)请求数据同步。 # 在 Redis 主从复制架构中,从节点会定期请求同步数据以保持数据一致。 3734:M 02 Mar 2025 04:35:23.995 * Replica 127.0.0.1:6380 asks for synchronization # 说明主节点接受了来自 127.0.0.1:6380 从节点的部分重同步请求。 # 部分重同步是 Redis 主从复制的优化机制,当从节点断开重连时,若主节点复制积压缓冲区有足够数据, # 可进行部分重同步,无需全量同步。这里主节点将从偏移量 880134 开始发送 0 字节的积压数据给从节点。 3734:M 02 Mar 2025 04:35:23.995 * Partial resynchronization request from 127.0.0.1:6380 accepted. Sending 0 bytes of backlog starting from offset 880134. # 表示 IP 地址为 127.0.0.1,端口号为 6381 的从节点向主节点请求数据同步。 3734:M 02 Mar 2025 04:35:28.759 * Replica 127.0.0.1:6381 asks for synchronization # 说明主节点接受了来自 127.0.0.1:6381 从节点的部分重同步请求, # 并将从偏移量 880134 开始发送 14 字节的积压数据给该从节点。 3734:M 02 Mar 2025 04:35:28.759 * Partial resynchronization request from 127.0.0.1:6381 accepted. Sending 14 bytes of backlog starting from offset 880134. #########################################################################################
测试主写从读
# 连接主节点 [vagrant@vbox ~]$ redis-cli -h 127.0.0.1 -p 6379 127.0.0.1:6379> set name niuzheng OK # 连接从节点读取 [root@vbox vagrant]# redis-cli -h 127.0.0.1 -p 6380 127.0.0.1:6380> get name "niuzheng" # 测试从节点写入禁止 [vagrant@vbox ~]$ redis-cli -h 127.0.0.1 -p 6380 127.0.0.1:6380> set name xiaozhao (error) READONLY You can't write against a read only replica.
启动哨兵
sudo -u redis redis-sentinel /usr/local/software/redis-7.2.4/config/sentinel-26379.conf
sudo -u redis redis-sentinel /usr/local/software/redis-7.2.4/config/sentinel-26380.conf
sudo -u redis redis-sentinel /usr/local/software/redis-7.2.4/config/sentinel-26381.conf
测试主故障转移
# 查看主节点的信息
[root@vbox vagrant]# ps -ef | grep 'redis-server.*:6379'
redis 3734 3732 0 04:35 pts/9 00:00:03 redis-server 127.0.0.1:6379
root 3826 3782 0 04:48 pts/12 00:00:00 grep --color=auto redis-server.*:6379
# 停止主节点,模拟主故障,查看哨兵日志
sudo kill -9 3734
# 解析哨兵日志
####################################sentinel日志######################################
# 进程 ID 为 3756,这里的 X 可能是日志记录的一种标识(正常 Sentinel 主进程标识为 'X' 表示它是 Sentinel 实例)。
# 时间为 02 Mar 2025 04:42:06.711,表明 Sentinel 实例启动并生成了自身的唯一 ID,该 ID 用于在 Sentinel 集群中标识自己。
3756:X 02 Mar 2025 04:42:06.711 * Sentinel ID is 07022b9fda7f507210f654a376c8572f5cf8cf94
# 时间为 02 Mar 2025 04:42:06.711,这行日志表示 Sentinel 开始监控名为 mymaster 的 Redis 主节点,
# 该主节点的 IP 地址是 127.0.0.1,端口是 6379,quorum 2 表示判断主节点下线至少需要 2 个 Sentinel 节点的同意。
3756:X 02 Mar 2025 04:42:06.711 # +monitor master mymaster 127.0.0.1 6379 quorum 2
# 时间为 02 Mar 2025 04:51:40.891,这行日志表示 Sentinel 检测到名为 mymaster 的主节点进入主观下线(Subjectively Down,sdown)状态,
# 即当前 Sentinel 实例认为主节点出现了故障。
3756:X 02 Mar 2025 04:51:40.891 # +sdown master mymaster 127.0.0.1 6379
# 时间为 02 Mar 2025 04:51:40.904,这行日志表示 Sentinel 检测到主节点状态变化后,将新的配置保存到了磁盘上,
# 以确保配置信息的持久化,防止重启后丢失。
3756:X 02 Mar 2025 04:51:40.904 * Sentinel new configuration saved on disk
# 时间为 02 Mar 2025 04:51:40.904,这行日志表示 Sentinel 生成了一个新的纪元(epoch),值为 3。
# 纪元用于在 Sentinel 集群中标识不同的故障转移(failover)周期。
3756:X 02 Mar 2025 04:51:40.904 # +new-epoch 3
# 时间为 02 Mar 2025 04:51:40.923,这行日志再次表明 Sentinel 将新的配置保存到磁盘上,可能是因为新的纪元生成等配置变化。
3756:X 02 Mar 2025 04:51:40.923 * Sentinel new configuration saved on disk
# 时间为 02 Mar 2025 04:51:40.923,这行日志表示当前 Sentinel 向 ID 为 1a77e77bcb0b0520d2403f8d83fe4a0f85e8f854 的 Sentinel 节点
# 投票,推举它为领导者来进行故障转移操作,纪元为 3。
3756:X 02 Mar 2025 04:51:40.923 # +vote-for-leader 1a77e77bcb0b0520d2403f8d83fe4a0f85e8f854 3
# 时间为 02 Mar 2025 04:51:40.992,这行日志表示主节点 mymaster 进入客观下线(Objectively Down,odown)状态,
# 即多个 Sentinel 节点都认为主节点出现了故障,#quorum 3/2 表示总共有 3 个 Sentinel 节点参与判断,至少 2 个同意才认定主节点客观下线。
3756:X 02 Mar 2025 04:51:40.992 # +odown master mymaster 127.0.0.1 6379 #quorum 3/2
# 时间为 02 Mar 2025 04:51:40.992,这行日志表示 Sentinel 计算出下一次故障转移的延迟时间,
# 即在此之前不会启动故障转移操作,时间为 Sun Mar 2 04:57:41 2025。
3756:X 02 Mar 2025 04:51:40.992 * Next failover delay: I will not start a failover before Sun Mar 2 04:57:41 2025
# 时间为 02 Mar 2025 04:51:41.666,这行日志表示 Sentinel 从 ID 为 1a77e77bcb0b0520d2403f8d83fe4a0f85e8f854 的 Sentinel 节点(IP 127.0.0.1,端口 26380)
# 接收到了配置更新信息,涉及的主节点是 mymaster,其 IP 地址为 127.0.0.1,端口为 6379。
3756:X 02 Mar 2025 04:51:41.666 # +config-update-from sentinel 1a77e77bcb0b0520d2403f8d83fe4a0f85e8f854 127.0.0.1 26380 @ mymaster 127.0.0.1 6379
# 时间为 02 Mar 2025 04:51:41.666,这行日志表示 Sentinel 执行了故障转移操作,将主节点从 127.0.0.1:6379 切换到了 127.0.0.1:6380。
3756:X 02 Mar 2025 04:51:41.666 # +switch-master mymaster 127.0.0.1 6379 127.0.0.1 6380
# 时间为 02 Mar 2025 04:51:41.666,这行日志表示将 127.0.0.1:6381 的 Redis 实例添加为新主节点(127.0.0.1:6380)的从节点,
# 并且该从节点关联的主节点名称为 mymaster。
3756:X 02 Mar 2025 04:51:41.666 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ mymaster 127.0.0.1 6380
# 时间为 02 Mar 2025 04:51:41.666,这行日志表示将 127.0.0.1:6379 的 Redis 实例添加为新主节点(127.0.0.1:6380)的从节点,
# 并且该从节点关联的主节点名称为 mymaster。
3756:X 02 Mar 2025 04:51:41.666 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6380
# 时间为 02 Mar 2025 04:51:41.683,这行日志表示 Sentinel 在故障转移操作完成后,将新的配置保存到了磁盘上,
# 以反映新的主从节点关系等配置信息。
3756:X 02 Mar 2025 04:51:41.683 * Sentinel new configuration saved on disk
# 时间为 02 Mar 2025 04:52:11.675,这行日志表示 Sentinel 检测到 127.0.0.1:6379 的从节点进入主观下线(sdown)状态,
# 该从节点关联的主节点是 127.0.0.1:6380,名称为 mymaster。
3756:X 02 Mar 2025 04:52:11.675 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6380
######################################################################################
# 重新启动6379机器
# 解析哨兵日志
####################################sentinel日志######################################
# 到了 2025 年 3 月 2 日 04:54:21.580,Sentinel 实例检测到之前处于主观下线状态的 127.0.0.1:6379 从节点恢复正常,
# 即退出了主观下线状态。这里的 "-sdown" 表示该节点不再处于主观下线的情况。
# 该从节点仍然关联到名为 mymaster 且地址为 127.0.0.1:6380 的主节点上。
3756:X 02 Mar 2025 04:54:21.580 # -sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ mymaster 127.0.0.1 6380
######################################################################################
汇总
监控阶段
用于同步各个节点的状态信息
- 获取各个sentine的状态(是否在线)
获取master的状态(属性和信息)
- 各个slave的状态(属性和信息)
解释说明
- 第一个哨兵连进去master节点,获取master节点的信息。
- 解析master节点信息,在第一个哨兵节点上保存master节点信息和slave节点信息,并且尝试连接每个slave节点。
- 在master节点上保存第一个哨兵信息。
- 第二个哨兵连进去master节点,获取master节点的信息。
- 解析master节点信息,在第二个哨兵节点上保存master节点信息和slave节点信息,并且尝试连接每个slave节点。
- 因为matser节点保存了第一个哨兵信息,所以第二个哨兵获取到了第一个哨兵信息,并尝试和第一个哨兵建立连接,双方把对方信息也保存到各自的配置文件,方便后续的信息交互。
通知阶段
- 哨兵之间互相通知,哨兵之间就相当于建了一个微信群聊,互通matser和slave节点的信息。
故障转移阶段
- 主观下线判定:当某个哨兵持续向 master 节点发送消息却未收到回应时,该哨兵会率先判定 master 节点为主观下线状态,并在 “群聊” 中发出询问,告知其他哨兵 master 节点疑似挂掉,呼吁大家一同检测。
- 客观下线确认:其他哨兵收到消息后,即刻尝试与 master 节点重新建立连接。一旦超过半数的哨兵均判定 master 节点无法正常连接,便会在 “群聊” 中确认 master 节点进入客观下线状态。
- 故障转移决策:确认 master 节点客观下线后,哨兵们会在 “群聊” 中发起故障转移讨论,以投票的形式决定由谁主导故障转移工作。当某个哨兵获得超过半数的选票时,它将肩负起处理故障转移的重任,保障系统的持续运行。
- 负责故障转移的哨兵,会依次检查 slave 节点状态,依优先级、复制偏移量筛选,节点 ID 排序,从中选出新的 master。
- 选出新 master 后,首要任务是将其角色从slave切换为master,重新配置相关参数,让其具备主节点功能。随后,负责故障转移的哨兵会迅速通知其他旧slave节点,使其断开与原master的连接,重新配置并接入新master,开始数据同步,保障集群数据一致性。与此同时,哨兵还会向客户端发布新maste 的地址信息,引导客户端请求转向新主节点,确保业务正常运行。此外,哨兵会持续监控整个集群状态,对新master及各slave节点进行健康检查,一旦发现新问题,及时启动相应的应对机制,保障Redis集群持续稳定运行。
拓展
多个哨兵之间怎么识别新增了其他哨兵,哨兵之间并没有互相配置信息?
- 如果配置文件里没有直接配置其他哨兵的信息,但是哨兵之间会通过master节点的信息来互相识别。
- 哨兵之间通过master节点的发送订阅消息来获取其他哨兵的信息,然后存储在自己的配置文件中。
如果一个哨兵还没开始订阅master节点的信息,master节点就挂了,这个哨兵就没办法获取到其他哨兵消息了?
- 不是绝对的,哨兵还可以基于配置信息初始探测其他哨兵的信息或者手动干预。
客户端如何在无感状态下切换主从节点?
- 哨兵会在故障转移的时候,发布新的master地址,客户端可以通过哨兵获取到新的master地址,然后切换到新的master节点。
这里以PHP为例,实现代码如下:
<?php # composer require predis/predis // 引入 Composer 的自动加载文件,使得我们可以使用Predis库 require'vendor/autoload.php'; // 引入 Predis 客户端类,用于与 Redis 服务器进行交互 use Predis\Client; // 引入 Predis 的 Sentinel 复制连接类,用于处理 Redis Sentinel 模式下的连接 use Predis\Connection\Replication\SentinelReplication; // 定义从节点选择策略接口,所有具体的选择策略类都需要实现这个接口 interface SlaveSelectionStrategy { // 定义一个抽象方法,用于从 SentinelReplication 中获取一个从节点 public function getSlave(SentinelReplication $replication); } // 使用 random_int 改进后的随机选择从节点策略类,实现了 SlaveSelectionStrategy 接口 class EnhancedRandomSelection implements SlaveSelectionStrategy { // 实现接口中的 getSlave 方法,用于随机选择一个从节点 public function getSlave(SentinelReplication $replication) { // 获取所有可用的从节点 $slaves = $replication->getSlaves(); // 如果没有可用的从节点,返回 null if (empty($slaves)) { return null; } // 获取从节点的数量 $count = count($slaves); // 使用 random_int 生成一个在有效索引范围内的随机索引 $randomIndex = random_int(0, $count - 1); // 返回随机选择的从节点 return $slaves[$randomIndex]; } } // 轮询选择从节点策略类,实现了 SlaveSelectionStrategy 接口 class RoundRobinSelection implements SlaveSelectionStrategy { // 用于记录当前轮询到的索引位置 private $index = 0; // 实现接口中的 getSlave 方法,用于轮询选择从节点 public function getSlave(SentinelReplication $replication) { // 获取所有可用的从节点 $slaves = $replication->getSlaves(); // 如果没有可用的从节点,返回 null if (empty($slaves)) { return null; } // 通过取模运算确保索引不会越界,选择当前索引对应的从节点 $selectedSlave = $slaves[$this->index % count($slaves)]; // 索引加 1,为下一次选择做准备 $this->index++; // 返回选择的从节点 return $selectedSlave; } } // 加权轮询选择从节点策略类,实现了 SlaveSelectionStrategy 接口 class WeightedRoundRobinSelection implements SlaveSelectionStrategy { // 存储每个从节点的权重 private $weights; // 记录当前轮询到的索引位置 private $index = 0; // 存储根据权重扩展后的从节点列表 private $weightedNodes = []; // 构造函数,接收从节点的权重配置 public function __construct($weights) { $this->weights = $weights; // 根据权重将从节点扩展到 weightedNodes 数组中 foreach ($weights as $node => $weight) { for ($i = 0; $i < $weight; $i++) { $this->weightedNodes[] = $node; } } } // 实现接口中的 getSlave 方法,用于加权轮询选择从节点 public function getSlave(SentinelReplication $replication) { // 获取所有可用的从节点 $slaves = $replication->getSlaves(); // 如果没有可用的从节点,返回 null if (empty($slaves)) { return null; } // 通过取模运算确保索引不会越界,选择当前索引对应的从节点 $selectedNode = $this->weightedNodes[$this->index % count($this->weightedNodes)]; // 索引加 1,为下一次选择做准备 $this->index++; // 遍历所有从节点,找到与选中节点匹配的从节点并返回 foreach ($slaves as $slave) { if ((string)$slave === $selectedNode) { return $slave; } } // 如果没有找到匹配的从节点,返回 null return null; } } // 最少连接数选择从节点策略类,实现了 SlaveSelectionStrategy 接口 class LeastConnectionsSelection implements SlaveSelectionStrategy { // 实现接口中的 getSlave 方法,用于选择连接数最少的从节点 public function getSlave(SentinelReplication $replication) { // 获取所有可用的从节点 $slaves = $replication->getSlaves(); // 如果没有可用的从节点,返回 null if (empty($slaves)) { return null; } // 初始化最少连接数为 PHP 能表示的最大整数 $leastConnections = PHP_INT_MAX; // 用于存储选择的从节点 $selectedSlave = null; // 遍历所有从节点 foreach ($slaves as $slave) { try { // 创建一个新的客户端连接到当前从节点 $client = new Client((string)$slave); // 获取从节点的客户端信息 $info = $client->info('clients'); // 提取连接的客户端数量 $connections = intval($info['connected_clients']); // 如果当前从节点的连接数小于最少连接数 if ($connections < $leastConnections) { // 更新最少连接数 $leastConnections = $connections; // 更新选择的从节点 $selectedSlave = $slave; } } catch (\Exception $e) { // 如果获取连接数失败,继续尝试下一个从节点 continue; } } // 返回选择的从节点 return $selectedSlave; } } // 响应时间选择从节点策略类,实现了 SlaveSelectionStrategy 接口 class ResponseTimeSelection implements SlaveSelectionStrategy { // 实现接口中的 getSlave 方法,用于选择响应时间最短的从节点 public function getSlave(SentinelReplication $replication) { // 获取所有可用的从节点 $slaves = $replication->getSlaves(); // 如果没有可用的从节点,返回 null if (empty($slaves)) { return null; } // 初始化最小响应时间为 PHP 能表示的最大整数 $minResponseTime = PHP_INT_MAX; // 用于存储选择的从节点 $selectedSlave = null; // 遍历所有从节点 foreach ($slaves as $slave) { try { // 创建一个新的客户端连接到当前从节点 $client = new Client((string)$slave); // 记录开始时间 $startTime = microtime(true); // 向从节点发送 ping 命令 $client->ping(); // 记录结束时间 $endTime = microtime(true); // 计算响应时间,转换为毫秒 $responseTime = ($endTime - $startTime) * 1000; // 如果当前从节点的响应时间小于最小响应时间 if ($responseTime < $minResponseTime) { // 更新最小响应时间 $minResponseTime = $responseTime; // 更新选择的从节点 $selectedSlave = $slave; } } catch (\Exception $e) { // 如果获取响应时间失败,继续尝试下一个从节点 continue; } } // 返回选择的从节点 return $selectedSlave; } } // 策略上下文类,用于管理和切换不同的从节点选择策略 class SlaveSelectionContext { // 存储当前使用的从节点选择策略 private $strategy; // 构造函数,接收一个从节点选择策略实例 public function __construct(SlaveSelectionStrategy $strategy) { $this->strategy = $strategy; } // 设置新的从节点选择策略 public function setStrategy(SlaveSelectionStrategy $strategy) { $this->strategy = $strategy; } // 调用当前策略的 getSlave 方法来选择从节点 public function getSlave(SentinelReplication $replication) { return $this->strategy->getSlave($replication); } } // 定义哨兵节点连接信息,用于连接到 Redis Sentinel 集群 $parameters = [ 'tcp://127.0.0.1:26379', 'tcp://127.0.0.1:26380', 'tcp://127.0.0.1:26381' ]; // 定义从节点权重,用于加权轮询策略 $weights = [ 'tcp://127.0.0.1:6380' => 2, 'tcp://127.0.0.1:6381' => 1 ]; // 创建不同策略实例 $enhancedRandomStrategy = new EnhancedRandomSelection(); $roundRobinStrategy = new RoundRobinSelection(); $weightedRoundRobinStrategy = new WeightedRoundRobinSelection($weights); $leastConnectionsStrategy = new LeastConnectionsSelection(); $responseTimeStrategy = new ResponseTimeSelection(); // 以下是对不同从节点选择策略进行测试的代码 // 改进后的随机选择策略测试 $enhancedRandomContext = new SlaveSelectionContext($enhancedRandomStrategy); $enhancedRandomOptions = [ 'replication' =>'sentinel', 'service' =>'mymaster', 'replication_strategy' => function (SentinelReplication $replication) use ($enhancedRandomContext) { return $enhancedRandomContext->getSlave($replication); } ]; $enhancedRandomClient = new Client($parameters, $enhancedRandomOptions); writeAndRead($enhancedRandomClient, 'enhanced_random_key', 'enhanced_random_value'); // 轮询选择策略测试 $roundRobinContext = new SlaveSelectionContext($roundRobinStrategy); $roundRobinOptions = [ 'replication' =>'sentinel', 'service' =>'mymaster', 'replication_strategy' => function (SentinelReplication $replication) use ($roundRobinContext) { return $roundRobinContext->getSlave($replication); } ]; $roundRobinClient = new Client($parameters, $roundRobinOptions); writeAndRead($roundRobinClient, 'round_robin_key', 'round_robin_value'); // 加权轮询选择策略测试 $weightedRoundRobinContext = new SlaveSelectionContext($weightedRoundRobinStrategy); $weightedRoundRobinOptions = [ 'replication' =>'sentinel', 'service' =>'mymaster', 'replication_strategy' => function (SentinelReplication $replication) use ($weightedRoundRobinContext) { return $weightedRoundRobinContext->getSlave($replication); } ]; $weightedRoundRobinClient = new Client($parameters, $weightedRoundRobinOptions); writeAndRead($weightedRoundRobinClient, 'weighted_key', 'weighted_value'); // 最少连接数选择策略测试 $leastConnectionsContext = new SlaveSelectionContext($leastConnectionsStrategy); $leastConnectionsOptions = [ 'replication' =>'sentinel', 'service' =>'mymaster', 'replication_strategy' => function (SentinelReplication $replication) use ($leastConnectionsContext) { return $leastConnectionsContext->getSlave($replication); } ]; $leastConnectionsClient = new Client($parameters, $leastConnectionsOptions); writeAndRead($leastConnectionsClient, 'least_connections_key', 'least_connections_value'); // 响应时间选择策略测试 $responseTimeContext = new SlaveSelectionContext($responseTimeStrategy); $responseTimeOptions = [ 'replication' =>'sentinel', 'service' =>'mymaster', 'replication_strategy' => function (SentinelReplication $replication) use ($responseTimeContext) { return $responseTimeContext->getSlave($replication); } ]; $responseTimeClient = new Client($parameters, $responseTimeOptions); writeAndRead($responseTimeClient,'response_time_key','response_time_value'); // 该函数用于向 Redis 主库写入数据,并从从库读取数据进行验证 function writeAndRead(Client $client, $key, $value) { try { // 主库写入操作 $writeResult = $client->set($key, $value); // 输出写入结果 echo "写入结果: ". ($writeResult? "成功" : "失败"). PHP_EOL; // 从库读取操作 $readResult = $client->get($key); if ($readResult === null) { // 如果读取结果为空,输出提示信息 echo "Key '$key' does not exist." . PHP_EOL; } else { // 输出从从库读取到的值 echo "Value read from slave: ". $readResult. PHP_EOL; } } catch (\Predis\Connection\ConnectionException $e) { // 捕获连接异常并输出错误信息 echo "连接错误: ". $e->getMessage() . PHP_EOL; } }