Redis从单机到集群实战:投票后端搭建与避坑指南
简介本资源为南京邮电大学大数据存储与管理课程实验一的完整报告文档面向正在学习Redis与大数据存储技术的高校学生及自学者帮助解决单机与集群环境搭建、以及基于Redis开发实际应用的问题。压缩包内共1个doc文件约933KB内容为结构完整的实验报告涵盖实验目的、环境配置、原理与步骤、结果分析及小结等模块。报告详细记录了在Ubuntu 16.04下安装配置Redis单机环境、搭建六节点集群并基于Redis构建文章投票网站后端的全过程包含关键命令、配置文件修改、Python代码实现与运行结果截图还整理了连接失败等常见问题的排错思路。目前已有324人学习适合需要参考实验流程、对照操作或查漏补缺的读者。1. 从一份实验报告说起Redis 单机到集群再到投票后端很多人第一次接触 Redis 都是在类似“大数据存储与管理”这样的课程实验里装完单机、配完集群、跑通一个投票后端就算交差了。但真正把这份实验报告拆开来看它其实是一条很完整的 Redis 上手链路从 Ubuntu 上装 Redis、改配置文件、用客户端连上去到搭一套 3 主 3 从的集群最后用 Python 写一个文章投票网站的后端把哈希、有序集合、集合运算全用上。这套东西麻雀虽小但把 Redis 最常用的几个数据结构和集群模式都串了一遍。如果你手头正好有这份实验代码或者想找一个能跑起来的 Redis 练手项目这篇笔记会按“装 → 连 → 集群 → 写业务 → 改代码”的顺序把里面每个环节的关键点和容易翻车的地方讲清楚。2. 环境准备与单机安装把 Redis 跑起来并连上2.1 安装命令逐条拆解实验环境是 Ubuntu 16.04安装方式走的是 Redis 官方 APT 源。这几条命令看着简单但每条都有它的作用少一条后面就可能出问题。# 安装 lsb-release、curl、gpg 三个基础工具 # lsb-release 用来获取当前系统代号如 xenialcurl 用来下载公钥gpg 用来处理密钥 sudo apt install lsb-release curl gpg # 下载 Redis 官方 GPG 公钥并转换成 apt 能识别的 keyring 格式 curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg # 把 Redis 官方源写入 apt 源列表$(lsb_release -cs) 会自动替换成系统代号 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 # 更新索引并安装 sudo apt-get update sudo apt-get install redis这里的关键点是signed-by参数它告诉 apt 用哪个公钥去验证这个源的包。如果公钥没导入成功apt-get update会直接报 NO_PUBKEY 错误。另一个容易忽略的是lsb_release -cs在 Ubuntu 16.04 上会输出xenial如果系统里没装lsb-release这条命令会失败源就写不进去。安装完成后启动服务# 启动 Redis 服务 /etc/init.d/redis-server start # 切到 root方便后面改配置和建目录 sudo -i2.2 配置文件改什么Redis 的主配置文件在/etc/redis/redis.conf。实验里用 vim 打开它但具体改了什么报告里没细说。按这个实验后续要连客户端、要搭集群的需求至少这几项要动# 绑定地址默认是 127.0.0.1只允许本机连 # 要让虚拟机外的客户端连进来改成 0.0.0.0 或者虚拟机的实际 IP bind 0.0.0.0 # 保护模式默认 yes外部 IP 直连会被拒绝 protected-mode no # 集群模式单机阶段保持 no搭集群时每个节点要改成 yes cluster-enabled no改完重启/etc/init.d/redis-server restart然后用ifconfig查一下虚拟机的 IP比如192.168.1.100后面客户端连接就用这个地址不要用127.0.0.1。2.3 客户端连接与那个经典的 ECONNREFUSED实验里用 Another Redis Desktop Manager 连虚拟机上的 Redis第一次连报的是RedisClient On Error: Error: connect ECONNREFUSED 127.0.0.1:6379 Config right?这个报错的意思是客户端在连自己本机的 6379但本机没跑 Redis。原因就是连接配置里地址填了127.0.0.1而 Redis 实际跑在虚拟机上。解决办法不是去关 Redis 的保护模式而是把客户端里的 Host 改成虚拟机的 IP 地址。报告里也记录了这个过程先按网上说的关保护模式没解决最后改 IP 才通。这个坑很典型本质是网络地址搞错了跟 Redis 配置关系不大。提示连不上时先ping一下虚拟机 IP再用redis-cli -h 虚拟机IP ping确认服务端可达最后才去查配置文件。3. 集群搭建6 个节点、3 主 3 从的配置与启动3.1 目录规划与配置文件生成集群部分的核心思路是一台机器上跑 6 个 Redis 实例端口分别是 6379、6380、6381主和 6389、6390、6391从每个实例用独立的配置文件和数据目录。# 创建集群根目录和日志目录 mkdir /opt/redis/cluster mkdir /opt/redis/cluster/log # 把默认配置复制过来作为模板 cp /etc/redis/redis.conf /opt/redis/cluster/ # 创建第一个主节点配置文件 cd /opt/redis/cluster touch redis-6379.conf vi redis-6379.confredis-6379.conf里需要写的内容按集群节点的要求关键项如下# 端口每个节点不同 port 6379 # 开启集群模式这是搭集群的前提 cluster-enabled yes # 集群配置文件Redis 自动维护每个节点要独立 cluster-config-file nodes-6379.conf # 节点超时时间毫秒 cluster-node-timeout 15000 # 数据目录每个节点独立避免冲突 dir /var/lib/redis/6379 # 日志文件 logfile /opt/redis/cluster/log/redis_6379.log # 后台运行 daemonize yes # 绑定地址 bind 0.0.0.0 # 关闭保护模式 protected-mode no其余 5 个配置文件6380、6381、6389、6390、6391内容一样只需要把port、cluster-config-file、dir、logfile里的端口号替换掉。这里有个细节dir目录必须提前存在否则 Redis 启动会失败。常见做法是启动前统一mkdir -p /var/lib/redis/{6379,6380,6381,6389,6390,6391}。3.2 启动 6 个节点并合成集群配置文件准备好后逐个启动/usr/bin/redis-server /opt/redis/cluster/redis-6379.conf /usr/bin/redis-server /opt/redis/cluster/redis-6380.conf /usr/bin/redis-server /opt/redis/cluster/redis-6381.conf /usr/bin/redis-server /opt/redis/cluster/redis-6389.conf /usr/bin/redis-server /opt/redis/cluster/redis-6390.conf /usr/bin/redis-server /opt/redis/cluster/redis-6391.conf检查是否都起来了ps -ef | grep redis应该能看到 6 个 redis-server 进程。然后用redis-cli --cluster create把它们合成一个集群/usr/bin/redis-cli --cluster create --cluster-replicas 1 \ 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表示每个主节点配 1 个从节点。Redis 会按顺序把前 3 个当主、后 3 个当从并自动分配 16384 个哈希槽。执行过程中会提示确认槽位分配方案输入yes即可。验证集群状态redis-cli cluster nodes cluster infocluster nodes会列出 6 个节点的角色、槽位范围和主从关系。cluster info里cluster_state:ok表示集群正常。3.3 集群搭建中的两个硬坑实验报告里记录了两个集群相关的报错都很典型。第一个(error) ERR This instance has cluster support disabled原因是 Redis 默认cluster-enabled no实例根本没开集群支持。解决就是把配置文件里的cluster-enabled改成yes去掉前面的#然后重启该节点。注意每个要加入集群的节点都得改漏一个就会在cluster create时卡住。第二个(error) ERR Slot 671 is already busy这个通常出现在之前搭过集群、数据没清干净的情况下。旧集群的槽位信息还留在nodes-*.conf里新集群分配槽位时就冲突了。解决办法是登录每个节点执行redis-cli -p 6379 flushall cluster reset6 个节点都做一遍然后删掉旧的nodes-*.conf文件重新执行cluster create。这个坑的血泪经验是搭集群失败后不要反复重试先把每个节点的状态清干净再重来。4. 投票后端核心代码哈希、有序集合与投票逻辑4.1 数据模型设计这个投票后端要支持的功能包括发布文章、给文章投票、按时间排序、按评分排序、文章分组。对应的 Redis 数据结构是这样安排的数据Redis 结构Key说明文章自增 IDStringarticle用 INCR 生成唯一 ID文章内容Hasharticle:id存 title、link、poster、time、votes发布时间排序ZSettimescore 为时间戳member 为文章 key评分排序ZSetscorescore 为评分member 为文章 key文章分组Setgroup:name存文章 key文章 Hash 里的字段包括title、link、poster发布者、time发布时间、votes票数。评分排序的 ZSet 用score作为 keyscore 值就是文章当前票数。4.2 post 函数逐段讲解实验报告里给出的关键代码是post函数import time import redis # 连接 Redisdecode_responsesTrue 让返回结果是字符串而不是 bytes rc redis.Redis(host192.168.1.100, port6379, decode_responsesTrue) def post(articleData, redisrc): # article 的数目加 1作为新文章 ID articleId str(redis.incr(article)) article article articleId # 把文章各字段写入 Hash for k, v in articleData.items(): redis.hset(article, k, v) # 记录发布时间加入 time 有序集合 post_time time.time() redis.zadd(time, {article: post_time}) print(即将为自己投初始票!, articleData[poster]) # 发布者给自己投一票 vote(article, articleData[poster], True) # 更新评分排序 score(article, redis) return article逐段看redis.incr(article)利用 Redis 的原子自增生成文章 ID多个客户端同时发布也不会撞号。hset循环把articleData字典里的字段写进 Hash这里articleData一般包含title、link、poster三个键。zadd(time, {article: post_time})把文章按发布时间排序后续可以用zrevrange取最新文章。vote(article, poster, True)里的第三个参数True表示这是发布者的初始自投票逻辑上要防止同一用户重复投。score(article, redis)负责把当前票数同步到评分 ZSet。4.3 vote 与 score 的配合vote函数的核心逻辑是用 Set 记录每篇文章的投票用户防止重复投票投票成功后更新文章的votes字段和score有序集合。def vote(article, user, is_initialFalse): # 用 Set 存投票用户key 为 article:id:votes voters_key article :votes # 如果用户已经投过直接返回 if not is_initial and rc.sismember(voters_key, user): print(f{user} 已经投过票了) return # 记录投票用户 rc.sadd(voters_key, user) # 文章票数加 1 rc.hincrby(article, votes, 1) # 更新评分排序 score(article, rc) def score(article, redisrc): # 从 Hash 里取出当前票数 votes redis.hget(article, votes) if votes is None: votes 0 # 把票数写入 score 有序集合 redis.zadd(score, {article: int(votes)})vote里用sismember判断用户是否投过sadd记录投票人hincrby原子增加票数。score函数每次从 Hash 读最新票数再写进 ZSet保证评分排序和实际票数一致。这里有个可以优化的点score每次都要读一次 Hash如果投票频繁可以用hincrby的返回值直接更新 ZSet少一次网络往返。4.4 分组与排序查询文章分组用 Set 实现把文章 key 加进group:namedef add_to_group(article, group): rc.sadd(group: group, article) def get_group_articles(group): return rc.smembers(group: group)按评分降序取文章def get_top_articles(n10): # zrevrange 按 score 从高到低取前 n 个 return rc.zrevrange(score, 0, n - 1, withscoresTrue)按时间降序取最新文章def get_latest_articles(n10): return rc.zrevrange(time, 0, n - 1, withscoresTrue)这两个查询是投票网站最常用的首页按评分排列表页按时间排。zrevrange的withscoresTrue会把分数一起返回方便前端展示票数或时间戳。5. 避坑与排查连接、集群、数据三类问题5.1 客户端连不上先查地址再查保护模式现象是客户端报ECONNREFUSED 127.0.0.1:6379。原因通常是客户端配置里 Host 填了127.0.0.1而 Redis 跑在虚拟机或远程机器上。解决是把 Host 改成 Redis 所在机器的实际 IP。如果改了 IP 还连不上再检查redis.conf里的bind和protected-mode把bind改成0.0.0.0、protected-mode设为no重启服务。注意不要一上来就关保护模式先确认地址对不对。5.2 集群创建报 cluster support disabled现象是执行cluster create时提示ERR This instance has cluster support disabled。原因是该节点的配置文件里cluster-enabled还是no。解决是打开对应节点的配置文件把cluster-enabled改成yes去掉注释重启该节点。6 个节点都要确认漏一个就会失败。5.3 槽位被占用Slot xxx is already busy现象是集群创建时提示某个槽位已被占用。原因是之前搭过集群nodes-*.conf和内存里的槽位信息没清干净。解决是登录每个节点执行flushall和cluster reset删掉旧的nodes-*.conf再重新创建集群。如果还不行把 6 个节点的数据目录整个清空重来。5.4 客户端连集群报 connection is close现象是用桌面客户端连集群时提示stream on error: connection is close或cluster is ended。原因是普通连接方式不支持集群的重定向。解决是客户端里改用 SSH 隧道加集群模式连接或者直接用redis-cli -c走命令行。-c参数会让 redis-cli 自动跟随 MOVED 重定向。5.5 数据目录不存在导致启动失败现象是redis-server启动后进程立刻退出日志里提示Cant chdir to /var/lib/redis/6379。原因是配置文件里dir指定的目录没建。解决是启动前统一创建所有节点的数据目录mkdir -p /var/lib/redis/{6379,6380,6381,6389,6390,6391}。这个坑在批量启动时特别容易漏。6. 二次开发从实验代码到能用的投票服务这份实验代码离一个能用的后端还有距离但改造方向很明确。我一般会从三个地方下手连接管理、投票防刷、查询分页。连接管理方面实验里是单机直连redis.Redis集群环境下要换成redis-py-clusterfrom rediscluster import RedisCluster # 集群节点列表startup_nodes 只需要给部分节点客户端会自动发现完整拓扑 startup_nodes [ {host: 192.168.1.100, port: 6379}, {host: 192.168.1.100, port: 6380}, {host: 192.168.1.100, port: 6381}, ] rc RedisCluster(startup_nodesstartup_nodes, decode_responsesTrue)decode_responsesTrue让返回的字符串直接可读不用每个地方都.decode()。集群模式下hset、zadd、sadd这些命令的用法和单机一样但跨槽位的多键操作会报 CROSSSLOT 错误所以设计 key 时尽量让相关数据落在同一个槽比如用article:{id}:votes这种带 hash tag 的命名。投票防刷方面实验里用 Set 记录投票用户但没限制投票时间窗口。实际场景可以加一个带过期时间的 Set或者用zadd记录投票时间戳超过一定时间的投票不计入。另一个思路是用 Lua 脚本把“检查是否投过 记录投票 增加票数”打包成原子操作避免并发下的重复投票。# 用 Lua 脚本保证投票的原子性 vote_script local voters_key KEYS[1] local article_key KEYS[2] local score_key KEYS[3] local user ARGV[1] if redis.call(SISMEMBER, voters_key, user) 1 then return 0 end redis.call(SADD, voters_key, user) local votes redis.call(HINCRBY, article_key, votes, 1) redis.call(ZADD, score_key, votes, article_key) return votes def vote_atomic(article, user): return rc.eval(vote_script, 3, article :votes, article, score, user)这个脚本把判断、记录、计票、更新评分四步合成一个原子操作并发下不会出现同一用户投两票的情况。eval的第一个参数是脚本第二个是 key 的数量后面依次是 key 和参数。查询分页方面zrevrange用start和end控制范围做分页时传(page-1)*size到page*size-1。如果要按时间范围查用zrevrangebyscore配合时间戳。文章内容多的时候Hash 里的字段可以拆成多个 key避免单个 Hash 过大。验证改造效果的方法很简单起两个 Python 进程同时给同一篇文章投票看最终票数是否等于实际投票人数。如果票数偏多说明防刷逻辑有并发漏洞如果票数正确再把集群停掉一个主节点看客户端是否能自动切换到从节点继续读写。这两个测试过了基本就能确认改造是有效的。从那以后我每次搭 Redis 集群都会先把 6 个节点的dir目录建好、cluster-enabled逐个确认、旧的nodes-*.conf清干净再执行cluster create这三步走完基本不会翻车。希望帮到你。本文还有配套的精品资源点击获取