从单机到三主三从:Redis集群与投票后端实验指南

发布时间:2026/11/3 14:01:32
从单机到三主三从:Redis集群与投票后端实验指南

简介:这份面向大数据存储与管理课程学习者的Redis实验报告文档,对应南邮实验一,适合正在接触内存数据库、键值存储与集群部署的本科或初学读者,用于完成从单机环境到集群搭建,再到简易文章投票网站后端实现的完整实验链路。文档共1个doc文件,压缩包约933KB,体量轻便,便于直接查阅和整理。内容围绕Ubuntu 16.04环境展开,涵盖Redis单机安装与配置、六节点主从集群组织、PyCharm下结合rediscluster库实现发帖投票与排序展示,并保留实验目的、环境、原理内容、结果分析与小结等报告结构,给出客户端连接验证与常见报错处理思路。已有324人学习下载,可作为实验预习、报告撰写、关键命令复盘与排错参考,帮助读者理解Redis在大数据场景中的缓存与键值存储用法。

1. 一份能直接跑通的 Redis 实验包,值不值得照着做

“redis 大数据”这个词在课程群里出现频率极高,但落到手上,多数人拿到的是一份截图比命令多的实验报告——装完了不会改配置,集群拉起来了不知道 16384 个槽是怎么分的,投票后端跑通了也说不出为什么用有序集合存分数。这份资源不一样:它把 Ubuntu 下的单机 Redis 安装、六节点三主三从集群、以及一个基于 Redis in Action 1.3 节思路的文章投票后端串成一条线,每个环节都留了真实命令和真实报错记录。适合三类人:要交实验但不想只抄截图的学生、第一次把 Redis 集群从零拉起来的后端新手、需要一份最小可跑的业务 demo 来验证 key 设计的工程师。它不讲数据结构语法,讲的是“装完能用、集群能连、业务能跑”这条链路上的每一步实际形态。

2. 单机先跑通:APT 源安装与 redis.conf 里真正要改的五个参数

单机这一步在实验报告里通常只有五行命令,但恰恰是后面所有故障的源头。我见过太多情况是:集群搭不起来,回头一查是单机阶段的bind没改;图形客户端连不上,回头一查是protected-mode还开着。所以这一章不急着敲命令,先把“为什么要这么装、每个参数动了会怎样”讲清楚,再落到可复制的脚本上。整个链路按“加源 → 装包 → 改配置 → 重启 → 三处验证”走,每一步都有明确的失败信号。

2.1 走官方源而不是系统源:keyring 与 apt update 的顺序

Ubuntu 自带的redis-server包版本往往偏旧,集群子命令的行为和新版有差异,所以实验里走的是 Redis 官方 APT 仓库。这个仓库的接入分三步,顺序不能乱,乱一步就报NO_PUBKEY或者404。

# 1. 装依赖:lsb-release 用来取发行版代号,curl 拉 key,gpg 做格式转换 sudo apt install -y lsb-release curl gpg # 2. 下载官方签名公钥,dearmor 转成 apt 能识别的二进制 keyring curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg # 3. 写入源列表,$(lsb_release -cs) 会展开成 xenial / bionic / focal 这类代号 echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list # 4. 刷新索引并安装 sudo apt-get update sudo apt-get install -y redis

逻辑上,第 2 步的gpg --dearmor是把 ASCII armored 的公钥转成二进制格式,apt只认后者;第 3 步的signed-by=是把这个 keyring 绑定到这一条源上,而不是全局信任,属于更克制的做法。参数-fsSL四个字母分别是:静默失败、跟随重定向、不输出进度、跟随跳转后仍按原地址校验,少一个都可能在某些网络环境下静默拿到空文件。

这里有个容易被忽略的点:lsb_release -cs返回的代号必须真的存在于官方源的dists/目录下。老发行版的代号如果已经不在索引里,apt-get update会直接抛 404,而不是报一个友好的“不支持”。遇到这种情况,常见做法是退回系统自带源安装,功能能跑通,但集群相关子命令的参数支持度会打折,后面--cluster create的行为要以redis-cli --version的实际输出为准,不要照抄教程。

装完之后先确认服务状态,Ubuntu 上/etc/init.d/redis-server start是 sysv 兼容脚本,底层仍然走 systemd,两条路都能起来,但重启和看日志建议统一用 systemctl,否则日志会分散在两处不好排查。

# sysv 兼容方式启动(实验里的写法) sudo /etc/init.d/redis-server start # 更推荐:systemd 方式,能直接看状态和日志 sudo systemctl enable --now redis-server sudo systemctl status redis-server --no-pager

2.2 redis.conf 里必须动的五项,以及动了之后的代价

配置文件默认在/etc/redis/redis.conf。这个文件有上千行,真正影响“能不能从外部连上、能不能被集群复用”的只有五项,其余绝大多数保持默认即可。下面这张表是我按实际影响面排的顺序:

参数默认值实验建议值为什么动它动了之后的代价
bind127.0.0.1 ::10.0.0.0或虚拟机内网 IP默认只监听回环,外部客户端连不进来暴露到网络上,必须配合防火墙或密码
protected-modeyesno无密码且非回环监听时,Redis 会拒绝外部连接同上,等于放弃一层兜底
daemonizenoyes前台运行会占住终端,集群多节点无法并行启动进程异常退出时终端看不到报错,只能看日志
dir.//var/lib/redis或自定义数据目录相对路径会跟随启动目录变化,重启后数据“消失”目录权限必须给 redis 用户可写
logfile""/var/log/redis/redis-server.log空值表示输出到 stdout,daemonize 后就丢了日志文件需要滚动策略,否则长期跑会涨

改法有两种:vim手工改看得清上下文,sed批量改适合写进脚本重复执行。实验环境里我一般先用sed打底,再grep复核一遍,防止某行被注释掉导致替换不生效。

CONF=/etc/redis/redis.conf # 只替换未被注释的行,^ 锚定行首可以避开注释行 sudo sed -i 's/^bind 127.0.0.1 ::1/bind 0.0.0.0/' $CONF sudo sed -i 's/^protected-mode yes/protected-mode no/' $CONF sudo sed -i 's/^daemonize no/daemonize yes/' $CONF # 这两项默认可能带引号或被注释,用 grep 先看实际形态再决定 grep -nE '^(# *)?(dir|logfile) ' $CONF # 改完必须复核,确认没有残留 grep -nE '^(bind|protected-mode|daemonize)' $CONF

^锚定行首这个细节值得多说一句:配置文件里大量出现# bind 127.0.0.1、# protected-mode yes这类注释示例,不带锚定会把注释行一起改掉,表面上没影响,实际上你改的是注释,真实配置纹丝不动,重启后一切照旧。这是排查“改了没用”时第一个要看的点。

改完重启,然后立刻做三件事:本机 ping、跨机 ping、端口监听确认。三件事对应三个不同的失败层次,缺一个就会在后面浪费半小时。

sudo systemctl restart redis-server # 1. 本机回环:验证进程本身活着 redis-cli ping # 2. 用虚拟机实际 IP:验证 bind 是否生效,IP 用 ifconfig 或 ip addr 查 redis-cli -h 192.168.x.x -p 6379 ping # 3. 看监听地址:0.0.0.0:6379 说明 bind 生效,127.0.0.1:6379 说明没改上 ss -lntp | grep 6379

2.3 图形客户端连不上,先分清是网络层还是协议层

图形客户端(实验里用的是 Another Redis Desktop Manager 这类工具)第一次连接失败,报错通常是ECONNREFUSED,含义很明确:TCP 层就没握上手。这时候不要急着去关安全模式,先按下面顺序排查,能省掉大量试错。

第一步看客户端填的地址。在虚拟机里跑服务、在宿主机上开客户端,客户端里填127.0.0.1指的是宿主机自己,当然连不上,必须换成虚拟机的实际 IP。实验报告里记录的正是这个坑,网上给的“关掉 protected-mode”方案没解决问题,因为病灶根本不在那一层。

第二步看虚拟机网络模式。NAT 模式下宿主机访问虚拟机的 IP 是通的,但虚拟机的 IP 可能每次重启都变,最稳的做法是在虚拟机里配静态 IP,或者用 host-only 网络固定网段。

第三步才是看bind和protected-mode。这两个改完没重启等于没改,systemctl restart之后再ss -lntp确认监听地址,比看配置文件可靠得多。

第四步,如果前三步都通了还报错,看服务端日志tail -f /var/log/redis/redis-server.log,拒绝连接的原因会明确写出来,比如“DENIED Redis is running in protected mode”。日志比任何猜测都准。

连接成功之后,建议在客户端里手工执行一次SET test:key hello再GET,确认读写都通,而不是只看连接状态是绿色的就以为完事。这一步验证的是权限和持久化路径,很多dir配错的情况就是在这里暴露的——写能成功,重启后数据没了。

3. 三主三从:六份配置只差三行,cluster create 的节点顺序别弄反

单机跑通之后进入集群。这一步的实验报告往往只给了六条启动命令和一条cluster create,中间“六份配置文件到底哪里不一样”被省略了,而这恰恰是后面Slot already busy、cluster support disabled这类报错的总根源。我的做法是先手写一份完整可用的模板,再用循环生成其余五份,最后统一启动、统一建集群、统一验收。

3.1 一份节点配置模板,逐项说清为什么

以 6379 这一份为例,模板内容如下,六份文件只有端口号不同:

# 对外端口,集群里每个节点必须唯一 port 6379 # 集群节点之间要互相通信,不能只监听回环 bind 0.0.0.0 # 后台运行,否则六个节点要开六个终端 daemonize yes # pid 文件按端口区分,不能共用 pidfile /opt/redis/cluster/redis-6379.pid # 日志按端口区分,方便单节点排错 logfile /opt/redis/cluster/log/redis_6379.log # 数据目录按端口区分,这一项最容易被忽略 dir /opt/redis/cluster/data/6379 dbfilename dump.rdb # 开启集群模式,这一行不写,后面建集群必报 disabled cluster-enabled yes # 节点自身维护的集群状态文件,由 Redis 自动生成和改写 cluster-config-file nodes-6379.conf # 节点心跳超时,内网实验环境 15 秒足够 cluster-node-timeout 15000 # 关闭保护模式,集群节点间通信不走密码 protected-mode no

dir这一项是踩坑重灾区。六份配置如果都写成默认的./或者同一个绝对路径,六个节点会往同一个dump.rdb和同一个nodes-*.conf上写,表现是:集群能建起来,但重启之后节点身份错乱,或者干脆起不来报“配置文件被占用”。按端口分目录是成本最低的规避方式。

cluster-config-file也有讲究。这个文件由 Redis 自己维护,内容是当前节点看到的集群拓扑,包括节点 ID、槽位归属、主从关系。它不能手工编辑,更不能在多个节点之间复制。手工复制会导致多个节点共享同一个节点 ID,建集群时报“节点 ID 冲突”或者直接卡在Waiting for the cluster to join。

3.2 批量生成与批量启动

手写六份容易抄错端口,用sed从模板生成更稳。关键是替换范围要覆盖port、pidfile、logfile、dir、cluster-config-file这五处,它们都以端口号作为区分标识。

BASE=/opt/redis/cluster mkdir -p $BASE/log $BASE/data # 把模板放在 $BASE/redis-template.conf,然后循环生成 for port in 6379 6380 6381 6389 6390 6391; do mkdir -p $BASE/data/$port # 全局把模板里的 6379 换成目标端口,模板中只在这五处出现 6379 sed "s/6379/$port/g" $BASE/redis-template.conf > $BASE/redis-$port.conf done # 复核:每份文件的端口、目录、日志路径都应该不同 grep -H -E '^(port|dir|logfile|cluster-config-file)' $BASE/redis-*.conf

全局替换的前提是模板里除这五处之外不能再出现6379字样,包括注释。所以模板里的注释不要写具体端口号,写“PORT”之类的占位符更安全。这一点属于血泪经验:注释里残留的端口号被替换掉,配置本身不会错,但你后面grep排查时会看到相互矛盾的信息,白白多绕一圈。

启动六个节点,然后确认进程数正好是六,多一个少一个都要停下来查:

for port in 6379 6380 6381 6389 6390 6391; do /usr/bin/redis-server $BASE/redis-$port.conf done # 期望输出正好 6 行(不含 grep 自身),多出来说明有残留进程 ps -ef | grep -c "[r]edis-server" # 逐个确认端口在监听,且监听地址是 0.0.0.0 ss -lntp | grep -E '6379|6380|6381|6389|6390|6391'

grep -c "[r]edis-server"这个写法里的方括号是技巧:让 grep 匹配redis-server但自身命令行里不出现纯字符串redis-server,从而不会把自己算进计数。不加这个括号,计数永远多 1,新手很容易以为多起了一个节点。

3.3 cluster create 的节点顺序决定主从分配

六个节点都起来之后执行建集群命令。--cluster-replicas 1表示每个主节点配一个从节点,六个节点正好三主三从。

/usr/bin/redis-cli --cluster create \ 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 \ 127.0.0.1:6389 127.0.0.1:6390 127.0.0.1:6391 \ --cluster-replicas 1

这条命令的节点顺序有实际含义:前三个作为主节点候选,后三个作为从节点候选,Redis 会按这个顺序做配对。执行后会打印一份分配方案并要求输入yes确认,确认前务必看清楚——如果六个节点里已经有残留的集群状态,方案可能和预期不符,这时候应该中断,去清理状态,而不是硬着头皮确认。

建完之后做三项验收,三项都过才算集群真的可用:

# 1. 集群整体状态:cluster_state 必须是 ok,槽位必须 16384 全覆盖 redis-cli -c -p 6379 cluster info # 2. 拓扑结构:应该看到 3 个 master、3 个 slave,且 slave 各自挂在某个 master 下 redis-cli -c -p 6379 cluster nodes # 3. 跨节点读写:写一个 key,看它是否被重定向到正确的槽 redis-cli -c -p 6379 set demo:key v1 redis-cli -c -p 6379 get demo:key

-c参数是集群模式的关键,它让客户端在收到MOVED重定向时自动跳到目标节点重试。不加-c,key 不在当前节点负责的槽位上就会直接返回MOVED 866 127.0.0.1:6380,看起来像报错,其实只是没开重定向。这个差异在写代码的时候更明显,后面第 4 章会专门讲客户端侧的对应处理。

cluster info里另一个要看的指标是cluster_known_nodes是否为 6。如果是 3,说明只发现了主节点,从节点还没完成握手,通常等几秒会恢复;如果一直是 3,去查对应节点的日志,多数是端口没通或者配置文件里cluster-enabled没生效。

4. 文章投票后端:用 hash、zset、set 三类结构把业务拆开

业务部分是整个实验里最有价值的一段,因为它把“Redis 的几种数据结构分别解决什么问题”落到了具体场景上。文章投票站的核心需求有四条:发布文章、给文章投票、按时间排序、按分组归类并按评分排序。这四条需求对应四种完全不同的访问模式,用同一种结构硬做会非常别扭,拆开之后就顺了。

4.1 数据模型:五个 key 各管一件事

key 形态类型存放内容对应需求
articlestring文章自增 ID 计数器发布时生成唯一编号
article:<id>hashtitle、link、poster、time、votes文章详情,按字段读写
timezsetmember 是文章 key,score 是发布时间戳按时间倒序取最新文章
scorezsetmember 是文章 key,score 是当前得分按评分排序、分页
voted:<article>set投过票的用户名防止重复投票
group:<name>set该分组下的文章 key分组归类

这套模型的关键判断是:为什么时间和分数要用两个 zset 而不是一个。因为排序维度不同,时间只写一次、永不更新,分数每次投票都要ZINCRBY,混在一个 zset 里就没法同时按两个维度取 Top N。分开存,代价是多一次写,换来的是两种排序都能做到 O(log N)。

为什么防重复投票用 set 而不是在 hash 里存一个计数。计数只能告诉你“有几票”,不能告诉你“谁投过”。用 set 的SADD返回值判断是否首次投票,天然幂等,这个模式在实际业务里非常常用。

4.2 post、vote、score 三个函数的实现与参数含义

下面这段是可直接运行的核心逻辑,单机版,连接参数按实际环境改。

import time import redis # decode_responses=True 让返回值是 str 而不是 bytes,省掉到处 decode rc = redis.Redis(host='192.168.x.x', port=6379, db=0, decode_responses=True) ONE_WEEK = 7 * 24 * 3600 # 投票有效期,秒 def post(article_data, r=rc): """发布文章:生成 ID、写 hash、入时间索引、给自己投初始票""" # INCR 是原子自增,多客户端并发发布也不会拿到重复 ID article_id = str(r.incr('article')) article = 'article:' + article_id article_data['time'] = time.time() article_data['votes'] = 1 # 作者自己的一票 # mapping 关键字参数要求 redis-py 3.5+,老版本用 hmset(article, article_data) r.hset(article, mapping=article_data) # 时间索引:score 用时间戳,后续按时间倒序直接 ZREVRANGE r.zadd('time', {article: article_data['time']}) # 分数索引先占位,避免 ZSCORE 返回 None 导致后续比较报错 r.zadd('score', {article: 1}) # 把自己记入已投票集合,防止作者重复投 r.sadd('voted:' + article, article_data['poster']) return article def vote(article, user, r=rc): """投票:先校验有效期,再用 SADD 返回值做幂等判断""" # 取发布时间,None 说明文章不存在 post_time = r.zscore('time', article) if post_time is None: return False # 超过一周的文章不再接受投票 if post_time < time.time() - ONE_WEEK: return False # SADD 返回 1 表示首次加入,0 表示已存在,天然防重 if r.sadd('voted:' + article, user) == 1: r.hincrby(article, 'votes', 1) r.zincrby('score', 1, article) return True return False def score(article, r=rc): """取单篇文章当前得分""" return r.zscore('score', article)

逻辑上有三个地方值得展开。第一处是INCR生成 ID,这是 Redis 做分布式 ID 生成器的最小用法,原子性由单线程模型保证,不需要额外加锁。第二处是sadd返回值做幂等,这个判断比“先查再写”可靠,因为查和写之间有空窗期,并发下会重复计票。第三处是先zadd('score', {article: 1})占位,而不是等第一次投票再创建——如果作者初始票走的是sadd分支之外的另一条路,zincrby也能从 0 开始建,但显式占位能让zscore永远有值,省掉一堆None判断。

参数上,ONE_WEEK是业务规则,改成ONE_DAY就能做日榜;hincrby的第二个参数必须是整数增量,写字符串会直接报类型错误;zincrby的分数可以是浮点,如果要做加权评分(比如票数除以时间衰减),把1换成算出来的权重即可。

4.3 分组归类与按评分取 Top N

分组用 set,因为分组不需要顺序,只需要“在不在这个组里”,SADD和SISMEMBER都是 O(1)。

def add_to_group(group, article, r=rc): # 幂等:重复添加同一篇文章不会产生重复成员 return r.sadd('group:' + group, article) def top_in_group(group, n=10, r=rc): """取分组内评分最高的 n 篇,做法是取交集后按分数排序""" # ZINTERSTORE 把 group 的 set 和全局 score 的 zset 求交,结果落到临时 key tmp = 'tmp:group:' + group r.zinterstore(tmp, ['group:' + group, 'score'], aggregate='MAX') result = r.zrevrange(tmp, 0, n - 1, withscores=True) r.delete(tmp) # 临时 key 用完即删,避免污染键空间 return result

zinterstore是这套模型里最关键的一条命令,含义是“把多个集合的交集存成一个新的有序集合”,其中 set 成员的默认分数按 1 处理,zset 成员保留原分数,aggregate='MAX'表示同一成员在多个输入里出现时取最大分数。这样得到的结果就是“这个分组里所有文章的分数排名”。

要注意tmp必须删。临时 key 如果忘了删,随着分组变多,键空间会被撑大,而且下次同名zinterstore会覆盖,表面上不出错,实际上多了一次无谓的写。

另一个容易忽略的点:zinterstore要求输入的所有 key 在同一个槽位上,单机模式无所谓,集群模式下不满足就会报CROSSSLOT Keys in request don't hash to the same slot。这一点在第 4.4 节展开。

4.4 换到集群模式,key 的落点决定代码能不能跑

单机跑通之后,把连接换成集群客户端,代码不是完全零改动。集群版客户端有两种主流的:

# 方案一:redis-py-cluster(实验里用的,包名 redis-py-cluster,导入名 rediscluster) from rediscluster import RedisCluster startup_nodes = [{"host": "192.168.x.x", "port": "6379"}] rc_cluster = RedisCluster(startup_nodes=startup_nodes, decode_responses=True) # 方案二:redis-py 自带的集群客户端(新版更推荐,导入路径以实际安装版本为准) from redis.cluster import RedisCluster as RedisClusterNew rc_cluster = RedisClusterNew(host='192.168.x.x', port=6379, decode_responses=True)

两种客户端都会自动处理MOVED和ASK重定向,startup_nodes只需要给集群里任意一个可达节点,客户端会自己拉CLUSTER SLOTS拿到完整拓扑。这里有个实际影响:只给一个种子节点是够的,但如果这个节点挂了,客户端启动时会失败,所以生产上一般把六个节点都写进startup_nodes。

真正需要改代码的是跨 key 操作。集群把 16384 个槽分散到三个主节点上,zinterstore涉及group:<name>、score、tmp三个 key,它们大概率不在同一个槽,直接报CROSSSLOT。解决办法是用 hash tag,把花括号里的内容作为哈希依据,保证同一组 key 落在同一槽:

# 用 {group:name} 作为 tag,三个 key 都会按 "group:name" 计算槽位 def top_in_group_cluster(group, n=10, r=rc_cluster): tag = '{group:%s}' % group group_key = tag + ':articles' score_key = tag + ':score' # 注意:这已经是分组内的分数副本,不是全局 score tmp = tag + ':tmp' r.zinterstore(tmp, [group_key, score_key], aggregate='MAX') result = r.zrevrange(tmp, 0, n - 1, withscores=True) r.delete(tmp) return result

这段改动的代价要说清楚:为了让 key 同槽,分组内的分数必须单独维护一份{group:x}:score,也就意味着一次投票要同时更新全局score和分组内的分数副本,写放大了一倍。实验规模下这个代价可以接受,但如果业务上分组很多、投票很频繁,更划算的做法是放弃服务端求交,改成客户端SMEMBERS拿文章列表、再用ZSCORE或 pipeline 批量取分、本地排序。数据量在几千篇以内,客户端排序的开销远小于写放大。

这个取舍是整章最值得记的一点:集群模式下,凡是涉及多 key 原子操作的命令,都要先问一句“这些 key 会不会同槽”,答案是否定的时候,要么用 hash tag 改写 key 设计,要么把计算搬到客户端,没有第三条路。

5. 避坑:四类报错背后的真实病灶

实验报告里记了四条问题,我按“症状最像、原因差得最远”的原则重新排了序,并补上了实际排查时该看的位置。这几条几乎覆盖了从单机到集群的全过程,遇到同类报错先对照这张表,比盲目搜要快得多。

5.1 客户端报 ECONNREFUSED,改了 bind 也没用

现象:图形客户端连接时报RedisClient On Error: Error: connect ECONNREFUSED 127.0.0.1:6379,按网上说法关掉protected-mode后仍然一样。

原因:报错里的127.0.0.1是客户端侧填的地址,指的是客户端所在机器,而不是虚拟机的地址。服务端bind改成0.0.0.0只是让服务监听所有网卡,和客户端填什么地址是两件事。

解决:先把地址改成虚拟机的实际 IP(ip addr或ifconfig查),再用ss -lntp | grep 6379确认服务端监听的是0.0.0.0而不是127.0.0.1,最后用redis-cli -h <虚拟机IP> ping在宿主机侧验证一次。三步都过再开图形客户端。这个顺序的意义是:每一层失败都有不同的报错形态,混在一起改会互相掩盖。

5.2 建集群报 cluster support disabled

现象:执行cluster nodes或--cluster create时报(error) ERR This instance has cluster support disabled。

原因:cluster-enabled默认是no,配置文件里通常写成# cluster-enabled yes的注释形态,看起来“有这一行”,实际没生效。用模板生成六份配置时如果漏了这一项,六个节点全都是单机模式。

解决:确认每一份端口配置里都有未被注释的cluster-enabled yes,改完必须重启该节点(不是 reload),重启后再redis-cli -p <port> info cluster看cluster_enabled:1。这一步要在建集群之前逐个节点验一遍,六份都过再执行--cluster create,能省掉一次全量返工。

5.3 建集群报 Slot 已经占用

现象:--cluster create执行到一半报[ERR] Node 127.0.0.1:6379 is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0.,或者是(error) ERR Slot 671 is already busy。

原因:这个节点上残留了上一次建集群的状态。残留可能在两处:nodes-<port>.conf记录的旧拓扑,以及dump.rdb里的旧数据。上一次建集群失败后没清理就重来,最容易撞上。

解决:登录每个节点,执行FLUSHALL清数据、CLUSTER RESET HARD清集群状态,然后停掉进程、删掉nodes-<port>.conf和dump.rdb,再重新启动。注意CLUSTER RESET要在节点处于集群模式时执行,如果节点还没开集群模式,直接删文件更干脆。

# 逐个节点清理,端口按实际改 for port in 6379 6380 6381 6389 6390 6391; do redis-cli -p $port flushall redis-cli -p $port cluster reset hard done # 停进程后删残留文件,再重启 pkill -f "redis-server.*redis-6" rm -f /opt/redis/cluster/data/*/nodes-*.conf rm -f /opt/redis/cluster/data/*/dump.rdb

cluster reset hard比soft更彻底:soft只清槽位和节点表,保留节点 ID 和 epoch;hard连节点 ID 一起重置,相当于变回一个全新的空节点。实验场景下用hard省心。

5.4 图形客户端连集群报 stream on error 或 cluster is ended

现象:单机连得好好的,切到集群模式后客户端报stream on error: connection is close、cluster is ended之类。

原因:图形客户端不一定实现了集群重定向协议,直连某个节点端口后,一旦该节点返回MOVED,客户端不知道该怎么跳,连接就断了。也有可能是客户端把集群当成单机连,端口填的是集群总线端口(普通端口 + 10000)而不是数据端口。

解决:实验里记的方案是走 SSH 通道再加集群连接,本质是让客户端只看到一个稳定的本地端口,重定向由隧道转发。另一个更省事的办法是干脆不用图形客户端看集群,改用redis-cli -c加--cluster check做验证,需要可视化时连单个节点看局部数据即可。这里没有银弹,选哪个取决于你是要看数据还是要看拓扑。

6. 把实验跑成能写进报告的证据:cluster check、slowlog 与一次小压测

实验做完、截图存好,很多人到这一步就收工了,但报告里“实验结果分析”那一节往往写不出东西,因为手上只有“跑通了”这一个结论。我的习惯是再补三步验证,把“跑通”变成“有数据支撑的跑通”,这三步都不复杂,但能让报告从“我操作了一遍”变成“我验证了这些指标”。

第一步是集群健康检查,一条命令出全量报告:

# 任意一个节点作为入口,会输出槽位分布、节点角色、迁移状态 redis-cli --cluster check 192.168.x.x:6379

这条命令的输出里有三个关键数字:16384 slots covered表示槽位全覆盖,缺一个就说明有主节点没分到槽;3 masters表示主节点数;每个 master 后面会列出它的 slave 和 slot 区间。如果看到[ERR] Not all 16384 slots are covered by nodes,不用慌,先看cluster info里的cluster_state,多数是某个节点刚重启还没完成握手,等十几秒再查一次。

第二步是用SLOWLOG看有没有意料之外的长耗时命令。投票站这种写多读少的场景,正常应该全是 O(1) 或 O(log N) 的操作,一旦出现几十毫秒的记录,基本是误用了KEYS、SMEMBERS全集遍历或者大 key 上的HGETALL。

# 把慢查询阈值调到 5 毫秒,只保留最近 128 条,便于实验观察 redis-cli -p 6379 config set slowlog-log-slower-than 5000 redis-cli -p 6379 config set slowlog-max-len 128 # 跑一段业务后再看 redis-cli -p 6379 slowlog get 10

输出里每条记录包含命令、耗时(微秒)、时间戳和客户端地址。我一般会重点看有没有zinterstore出现在慢日志里——分组多、成员多的时候它是 O(N*K),容易成为瓶颈,这时候就该考虑第 4.4 节说的客户端排序方案了。

第三步是做一次小规模并发写,验证集群下的重定向和写入分布。不需要压测工具,用redis-benchmark做定向测试就够:

# -c 并发 50,-n 总请求 10000,-t 只测 set/get,-P pipeline 每条 16 个命令 redis-benchmark -h 192.168.x.x -p 6379 -c 50 -n 10000 -t set,get -P 16 -q

-q只输出汇总行,适合直接贴进报告。这里要看的不是绝对 QPS 数值(虚拟机环境下的数字没有参考意义),而是set和get的耗时分布是否在同一量级。如果get明显慢于set,常见原因是数据分布不均导致某些槽位热点,用redis-cli --cluster check看槽位分布是否均匀,再看cluster nodes里各节点的 key 数量。

最后一步是验证持久化,这一步最容易被跳过,但也最容易在答辩时被问到。做法很简单:往集群写一批 key,然后依次重启三个主节点,再看数据是否还在。

# 写 100 个测试 key for i in $(seq 1 100); do redis-cli -c -p 6379 set bench:$i v$i > /dev/null; done # 重启一个主节点,等它重新加入集群 redis-cli -p 6379 shutdown /usr/bin/redis-server /opt/redis/cluster/redis-6379.conf sleep 5 # 抽查数据是否完整,DBSIZE 应该在集群各节点上分别看 redis-cli -c -p 6380 get bench:50

这里有个细节:shutdown之后不要立刻启动,先确认端口释放,否则新进程起不来报端口占用。另外,如果没开 AOF,重启之间写入的数据可能因为dump.rdb的保存策略而丢失一部分,看到数据少了不要急着判定为故障,先看redis.conf里的save配置和appendonly状态。这个差异正好可以写进报告的“实验结果分析”:RDB 和 AOF 在重启场景下的表现不同,实验里默认配置对应的是哪种行为。

那条“cluster node 重启后身份错乱”的坑我就是这么踩出来的——当时六份配置共用了同一个dir,重启一个节点后它的nodes-*.conf被另一个节点覆盖,集群直接认不出它。从那以后我每次生成节点配置,都强制走一遍grep -H -E '^(port|dir|logfile|cluster-config-file)'复核,六份输出逐行对一遍,确认端口、目录、日志、拓扑文件四项两两不同,才敢启动。这条看起来笨的检查,比事后翻日志省太多时间。希望帮到你。

本文还有配套的精品资源,点击获取