凌晨两点,告警说 Redis 延迟突然翻了十倍。你打开监控,发现内存还有一半,于是第一反应可能是:“再加两台机器吧。”
先别急。Redis 扩展最容易犯的错,就是把所有问题都叫作“容量不够”。如果真正的原因是一条慢命令、一个超级热点 key,或者应用突然建立了几十万条短连接,加节点只是把问题藏到更复杂的拓扑里。等下一次故障再来,你还得同时处理复制延迟、节点选举和客户端路由,现场只会更乱。
这一章不从架构名词开始。我们先学会识别瓶颈,再依次解决读吞吐、可用性、写吞吐和内存容量,最后把迁移、监控、演练与回滚串成一套能落地的流程。
扩展回答的是“怎样让更多节点共同承担工作”,高可用回答的是“某个节点坏了以后怎样继续服务”,持久化与备份回答的是“数据损坏或误删以后怎样恢复”。三者会互相影响,却不是同一件事。

我们可以把 Redis 的扩展目标拆成三条轴:读吞吐、写吞吐、内存容量。每条轴对应的手段不同。
真正有用的容量基线不是“Redis 能跑十万 QPS”这种孤立数字,而是一组带业务条件的数据:生产 key 的大小分布、命令比例、流水线批量、连接数、持久化方式,以及你关心的 P95/P99 延迟。
变更前至少保留这些观察项:
INFO memory:看 used_memory、used_memory_rss、碎片与淘汰趋势;INFO stats:看命中、未命中、过期、淘汰、拒绝连接和网络字节;INFO commandstats:看各类命令调用次数和平均 CPU 时间;INFO replication 与 ROLE:看复制关系、偏移量和链路状态;SLOWLOG GET 与 LATENCY DOCTOR:定位慢命令及服务端延迟事件;redis-cli --bigkeys 或 --keystats:用渐进扫描抽样大 key,别在生产环境用 KEYS * 做盘点。如果问题来自 big key,分片后这个 key 仍然只属于一个槽、一个节点。分片不会自动把一个巨大的 Hash 或 Sorted Set 切成多份。迁槽时它还会放大阻塞与网络抖动,所以要先从数据模型上拆 key。
假设三个 primary 平均分担数据,看上去每个节点用到 70% 内存似乎还能接受。可一旦某节点故障、扩容迁槽或数据短时倾斜,剩余节点就可能没有接住负载的空间。
容量预算至少要包含:
如果你只能记住一个判断顺序,可以用下面这套:
先排除慢命令、热点 key、big key、连接风暴、swap 和持久化抖动。它们不是靠横向扩展就能自然消失的问题。
再把压力拆成读、写和内存三类。读多通常先考虑副本或本地缓存;写与容量需要让不同 key 落到不同 primary。
然后明确允许的数据新鲜度、可接受的写丢失窗口、恢复时间和恢复点。没有这些边界,就无法判断异步复制是否合适。
最后才选择 Sentinel、Redis Cluster、代理或托管服务,并把客户端能力、监控与演练成本一起算进去。
复制的直觉很简单:primary 负责写,replica 跟随 primary 的数据历史。读多写少时,你可以把允许短暂陈旧的读取交给 replica,减轻 primary 的读压力。
可复制不是“每次 SET 都同步写进两台机器再返回”。Redis 默认采用异步复制。primary 执行写入后通常先向客户端返回,再把产生的数据变更作为复制命令流发给 replica。这个选择换来了低延迟,也留下了一个失败窗口:primary 已经回复成功,但 replica 还没收到这条写入时,primary 突然故障,新 primary 可能没有这条数据。

每段复制历史可以用两个量来理解:复制 ID 标识“这是哪一段数据历史”,复制偏移量标识 replica 已经追到了这段历史的哪个位置。
primary 每产生一些复制流,偏移量就向前推进。replica 周期性报告自己已经处理到的偏移量。于是 primary 可以同时判断 replica 是否在线,以及它落后多少字节。
这比只看“连接状态为 up”有用得多。链路可以是 up,replica 却因为网络、CPU 或磁盘加载而持续落后。对读副本和故障转移来说,落后量才决定数据有多旧。
primary 会在内存中保存一段最近产生的复制流,这块环形缓冲区就是 replication backlog。replica 断线重连时,会带着旧复制 ID 和自己最后处理的偏移量发起 PSYNC。
如果两个条件都成立,primary 就能执行部分同步:
此时 primary 只补发缺口,不必重新传全部数据。你可以把 backlog 看成录像机的回看窗口:断网期间的节目还在缓存里,就从断点继续;已经被新内容覆盖,只能重新拿一份完整录像。
一个粗略但实用的估算是:
比如高峰期复制流约为 20 MB/s,希望承受 60 秒网络抖动,安全系数取 1.5,那么 backlog 至少应按 1.8 GB 量级评估。这里要用复制字节速率,而不是“每秒命令数”;一次写入可能只有几十字节,也可能改动几百 KB。
当缺口已被覆盖,或者复制历史对不上,replica 必须全量同步。经典流程包含四段工作:
这不只是“多传一点网络”。快照会带来 fork、内存页写时复制、磁盘或网络流量;replica 加载快照时也可能暂时无法正常服务。数据集越大,反复全量同步越像一场自我放大的事故:同步占资源,资源紧张导致链路继续超时,又触发下一次同步。
磁盘较慢或希望减少中间落盘时,可以评估无盘复制,让子进程把 RDB 数据直接通过网络发送给 replica。repl-diskless-sync 改变的是全量传输路径,不会消除生成快照、网络发送、replica 加载和期间缓冲写入的成本。
只配置 backlog 不监控,相当于买了保险却不知道保额。INFO replication 中可以观察 backlog 是否启用、当前历史长度、复制偏移量和 replica 状态。
如果预期网络抖动 90 秒,而当前高峰写入只需 20 秒就能覆盖整块 backlog,那么“部分同步能力”实际只够 20 秒。扩容、跨可用区部署和批量重启之前都要重新计算,因为写流量与网络条件已经变了。
把读取交给 replica 之前,先问业务能不能接受陈旧数据。商品介绍、排行榜展示通常可以容忍几百毫秒偏差;刚修改完的权限、库存扣减结果、一次性验证码状态往往不行。
常见失败模式有四种:
所以读写分离不应该只是“写连一个地址,读随机连三个地址”。客户端需要感知角色变化,健康检查要包含复制延迟,强依赖新鲜数据的读取仍然走当前 primary。
WAIT 能缩小窗口,但不是强一致开关对少量更重要的写入,客户端可以在写入后调用 WAIT,等待指定数量的 replica 确认已经收到相应复制流。它能降低 primary 刚返回就故障时丢写的概率,却不会把 Redis 变成严格的强一致系统。
原因很现实:replica 确认收到复制流,不等于数据一定按你的持久化要求安全落盘;复杂网络分区和选举仍可能让另一个落后的 replica 被提升。WAIT 还会增加写延迟,所以应按业务等级使用,而不是给所有缓存写入统一加上。
如果你更关心“primary 和 replica 失联后不要继续无限接收写”,可以设置:
min-replicas-to-write 1
min-replicas-max-lag 10它表示:至少有一个延迟未超过 10 秒的 replica,primary 才接受写入。这是一道限制风险窗口的护栏,不是逐条写入的强一致确认。网络抖动时它还会牺牲可用性,主动拒绝写入。
复制能增加读取副本和故障接管候选,但每个 replica 都保存完整数据集。给一个 100 GB 的 primary 增加三个 replica,并不会把单机容量变成 400 GB;它只会得到四份相近的数据副本。要突破单个数据集容量,必须分片。
只有复制时,primary 坏了还得有人判断故障、选择 replica、执行提升,并告诉客户端新地址。Redis Sentinel 就是这套非分片高可用控制面。
你可以把 Sentinel 理解成值班小组,它不承载业务 key,主要负责四件事:

单个 Sentinel 在规定时间内联系不到 primary,会先认为它主观下线,也就是“我看它不通”。这可能只是 Sentinel 自己所在网络有问题,所以不能立刻切换。
当达到 quorum 数量的 Sentinel 都认为 primary 不可达,才形成客观下线判断,也就是“足够多观察者都看不到它”。随后还要有一个 Sentinel 获得 Sentinel 总体的多数票授权,才能真正发起这轮故障转移。
这两个条件经常被混淆:
quorum 决定多少 Sentinel 同意后,可以把 primary 判为客观下线;假设共有 5 个 Sentinel,quorum 设为 2。两个 Sentinel 一致认为 primary 下线,可以触发故障判断;但至少要能获得 3 票,故障转移才会真正开始。这样可以避免网络被对称切成两半后,两边各自提升一个新 primary。
在概念层面,一轮 Sentinel 故障转移会经历这些动作:
多个 Sentinel 先独立检测故障,从主观下线汇聚成客观下线。
Sentinel 通过投票选出本轮领导者,避免多个控制者同时发起不同切换。
领导者从可用 replica 中选择候选。复制状态、优先级和复制偏移都会影响选择,落后太多或状态异常的 replica 不应被当作理想候选。
候选 replica 被提升为新 primary,其他 replica 改为跟随新 primary。
Sentinel 不是一个透明代理。应用仍然直接连接 Redis 数据节点,只是在建立或重建连接时向 Sentinel 查询当前 primary。
一个合格的接入过程至少要做到:
多数授权能避免 Sentinel 在两个少数派里随意各选一个 primary,却不能让旧 primary 瞬间消失。
想象 primary 与一个客户端一起被隔离在少数派网络,另两个 Sentinel 和 replica 位于多数派。多数派会提升新 primary,服务继续运行;但少数派客户端可能还在向旧 primary 写入。网络恢复后,旧 primary 会改成新 primary 的 replica,它在隔离期间收到的写入会被丢弃。
前面提到的 min-replicas-to-write 与 min-replicas-max-lag 可以让旧 primary 在失去足够 replica 后停止接收写,缩短这个窗口。不过它以“网络分区时宁可拒写”为代价,而且异步复制仍不能保证零丢失。
至少三个 Sentinel 的意义是获得独立故障视角。如果三个 Sentinel 都跑在同一台宿主机、同一个故障域里,这台机器一断电,监控与投票一起消失。
部署时要让 Sentinel 跨宿主机或故障域分散,并验证节点对外公布的地址确实能被其他 Sentinel、Redis 节点和客户端访问。容器端口映射、NAT 和错误的 announce 地址,很容易造成“进程都活着,但大家发现的是不可达地址”。
Sentinel 解决的是单个 primary 数据集的自动接管,不负责把 key 分散到多台 primary。写吞吐和数据容量已经超过单节点时,应进入 Redis Cluster 或其他分片方案,而不是继续堆 replica。
当瓶颈变成写吞吐或数据容量,关键动作是让不同 key 由不同 primary 负责。Redis Cluster 原生提供数据分片与分片级故障转移。
这里先纠正一个很常见的说法:Redis Cluster 不使用一致性哈希环。它先把 key 映射到固定的 16384 个 hash slot,再把这些槽分配给各个 primary。
比如 A 负责槽 0~5460,B 负责 5461~10922,C 负责 10923~16383。扩容时不是把所有 key 重新做一次“节点数取模”,而是把一部分槽及槽内 key 从旧节点迁到新节点。

槽把“key 到节点”的映射拆成两步:
key → 固定编号的槽 → 当前负责这个槽的节点节点变化时,key 到槽的计算不变,只需调整少量“槽属于谁”的关系。客户端缓存的也是槽到节点的拓扑表。这让在线扩容、缩容和负载再平衡有了统一操作单位。
16384 是槽的总数,不代表应该部署 16384 个 primary。真实集群规模还受网络、故障传播、运维复杂度和业务负载约束。
默认情况下,整个 key 都参与 CRC16。如果 key 中出现有效的 {...} 片段,Cluster 只对第一组有效花括号里的内容计算槽。
cart:{u42}:items
cart:{u42}:coupon这两个 key 都只对 u42 计算,所以必然落到同一槽。这样你可以在一次 MGET、事务或 Lua 脚本中同时操作它们。
但 hashtag 不是“能加就加”。如果所有购物车都写成 {cart}:u42、{cart}:u43,它们会全部挤进同一个槽,整个集群又退化成一个热点 primary。更合理的 tag 通常是能表达最小原子业务边界的标识,比如用户 ID 或订单 ID。
Redis Cluster 处理一个命令、一个事务或一段 Lua 脚本时,涉及的所有 key 通常必须属于同一槽。否则客户端会收到 CROSSSLOT。
这意味着迁移到 Cluster 前,必须盘点:
MGET、MSET、集合交并差是否跨业务实体;MULTI/EXEC 是否同时修改不同用户或不同订单的 key;有三种常见改法:把需要原子处理的数据建模到同一 key;用精确 hashtag 让少量相关 key 同槽;把跨槽流程上移到应用,用幂等、补偿或消息流程处理。不要为了保留一个跨全局的 Lua 脚本,把所有 key 强行塞进同一槽。
每个 Cluster 节点通常有两个端口:客户端命令端口,以及节点间使用的 cluster bus 端口。bus 用于故障检测、配置传播、故障转移授权等内部通信。
防火墙只开放客户端端口、漏掉 bus 端口时,应用可能连得上个别节点,但集群无法正常形成一致拓扑。容器或 NAT 环境还要保证节点公布的地址和端口能被其他节点与客户端实际访问。
支持 Cluster 的客户端会先获取槽拓扑,计算目标 key 的槽,再直接把命令发给负责节点。集群稳定时,大部分请求只需一次网络往返。
如果客户端拓扑过期,节点会返回重定向:
MOVED 表示槽的正式归属已经改变。客户端应重试命令,并刷新槽映射;ASK 表示槽正在迁移,这个 key 暂时去目标节点找。客户端只对当前命令先发 ASKING 再重试,不应立刻把槽永久改归目标节点。客户端还应能从 CLUSTER SHARDS 获取当前分片、节点角色和端点信息,并在拓扑变化后更新连接池。只会捕获 MOVED 字符串、不会处理 ASK 的自制客户端,在在线迁槽时迟早会暴露问题。
Cluster 的 replica 默认会把请求重定向到负责该槽的 primary。客户端可以对连接执行 READONLY,表示愿意从 replica 读取可能陈旧的数据。
这仍然要求客户端理解槽拓扑:replica 只能服务其 primary 所负责的槽;迁槽或故障转移后,原有映射可能失效。强依赖新鲜状态的读不要因为“有 replica”就自动下放。
每个分片通常由一个 primary 和一个或多个 replica 组成。primary 故障后,符合条件的 replica 通过集群投票被提升,接管这组槽。
复制仍然是异步的,所以 primary 已确认但尚未传播到获选 replica 的写入可能丢失。网络分区中的少数派 primary 也只能在有限窗口内继续服务;超过 cluster-node-timeout 相关窗口且无法联系多数 primary 后,它会停止接受请求。
如果多数 primary 不可用,Cluster 无法继续正常工作。默认配置还要求所有槽都有覆盖;只要有一部分槽无人负责,集群就会进入失败状态,避免在自己看不全拓扑时继续返回可能误导的结果。
给 Cluster 加一台空节点并不会自动增加容量。新节点一开始没有槽,也就没有业务 key。真正的扩容动作是从现有 primary 取出一批槽,逐个把槽内 key 迁到新节点。

把槽 742 从 A 迁到 B,可以这样理解:
IMPORTING,准备接收来自 A 的 key;MIGRATING,不再把所有请求都当成普通稳定槽处理;MIGRATE 搬到 B;ASK 临时转到 B;MOVED 更新槽表。ASK 和 MOVED 的区别很像临时窗口与永久迁址:前者只告诉你“这一次去隔壁窗口”,后者告诉你“这个业务以后归新地址”。
MIGRATE 对单个 key 的转移具有明确的源、目标和删除顺序,但搬运本身需要序列化数据、走网络、在目标恢复对象。一个几十 MB 的大 key 不会因为“按槽迁移”就自动拆成小块,它可能让相关节点在迁移期间出现明显延迟。
生产重分片前要先回答:
ASK、ASKING、MOVED 和拓扑刷新;TRYAGAIN 时是否会有限重试;即使几个 key 原本属于同一槽,迁移过程中它们可能一部分还在源节点、一部分已经到了目标节点。这时涉及不存在 key 或跨源目标状态的多 key 操作可能返回 TRYAGAIN。
客户端可以短暂退避后重试,但要设置次数和总时长。无限重试会把一次迁槽抖动变成连接池堆积与请求雪崩。真正要求稳定原子性的热点流程,应避开对应槽的迁移窗口,或者在变更前暂停相关批任务。
平均分配槽数量不等于平均分配负载。一个槽可能只有几十个冷 key,也可能装着热门用户或大型集合。
因此再平衡至少同时看:
如果热点来自一个不可拆的大 key,继续移动槽只是把热点从 A 搬到 B。真正的修复仍是重新设计 key 粒度,或在应用侧增加局部缓存、请求合并和限流。
扩展方案的区别不只在性能,更在“拓扑变化和故障由谁处理”。这一点比纸面 QPS 更影响长期成本。

手写 hash(key) % N 看起来只有一行代码,节点数一变却会让大量 key 改变目标。Python、Java、Go 的默认 hash() 还可能有不同实现或进程随机化,不能当作跨语言稳定分片协议。
即便使用一致性哈希,你仍要自己解决:
这不是不能做,而是要承认你正在维护一套分布式数据路由协议。
代理能让应用继续像连接单个 Redis 地址一样工作,适合大量旧客户端。但代理必须扩展连接数、吞吐和故障域,否则它会变成新的瓶颈或单点。
还要逐条确认命令语义。某些代理能拆分跨分片 MGET 再聚合结果,却无法让跨分片事务突然具有原子性。批量命令、Pub/Sub、阻塞命令、脚本和模块命令的兼容范围,都不能靠“看起来能连上”来推断。
托管服务可以接管很多控制面工作,但应用仍要面对数据模型、热点、big key、重试幂等和一致性窗口。购买前要问清:
先问“失败时由谁发现、谁决定、谁重试、谁回滚”,再比较正常状态下的吞吐。很多方案在一切顺利时只差一次网络跳转,真正的差距会在扩容和故障的十分钟里出现。
一套拓扑在架构图上成立,不代表生产故障时能工作。复制可能因为 backlog 太小反复全量同步,Sentinel 可能发现了不可达地址,Cluster 客户端可能从未走过 ASK 分支。只有演练能把这些隐藏条件提前暴露出来。

观察 CPU、内存、RSS、碎片、网络、连接数、阻塞客户端、命令延迟、慢日志、过期和淘汰。不要只看平均值,迁槽和全量同步更容易先抬高 P99。
观察 primary 与 replica 的连接状态、复制偏移差、backlog 历史长度、部分/全量同步次数,以及 replica 是否长期落后。一次全量同步不可怕,短时间反复发生才是危险信号。
Sentinel 侧观察主观/客观下线、投票、故障转移耗时、地址发现结果。Cluster 侧观察 cluster_state、16384 槽覆盖、PFAIL/FAIL 节点、迁移中槽、每个 primary 的 replica 数量和拓扑版本变化。
观察超时率、重试率、MOVED、ASK、TRYAGAIN、CROSSSLOT、只读错误、连接池耗尽和业务 P99。Redis 自己延迟正常时,客户端错误与重试也可能把页面拖慢。
在隔离环境先做,再在有停止线的生产窗口小心演练:
记录当前 primary、replica 偏移、Sentinel 数量、quorum、应用连接地址和业务错误率,确认备份可用。
用可控方式让 primary 暂时无响应,而不是直接破坏数据目录。观察主观下线、客观下线、投票和候选选择。
验证新 primary 可写,其他 replica 已改为跟随它,客户端能通过 Sentinel 发现新地址,旧连接没有继续写入。
恢复旧 primary,确认它以 replica 身份加入并追平。不要因为机器名里带“主库”就手工把角色抢回去。
Cluster 至少要分别演练“单个 primary 故障”和“在线迁槽”。前者验证 replica 选举与客户端拓扑刷新,后者验证 ASK/MOVED、迁移速率、big key 和多 key 重试。
迁槽建议从少量低风险槽开始:
扩容中的回滚对象可能已经变化。比如一半 key 已从 A 到 B,这时简单删除 B 或把流量切回 A 会直接丢数据。
一份可执行的回滚方案要写清:
MIGRATING/IMPORTING;replica 会忠实复制误删除、错误脚本和被污染的数据。Cluster 也会把错误写入正确路由到负责槽。自动故障转移解决不了“所有副本都一致地错了”。
所以你仍需要与业务目标匹配的 RDB/AOF 策略、离线备份、恢复验证和权限隔离。备份“存在”不等于“能恢复”;定期把备份恢复到隔离环境,并校验 key 数、关键业务数据和恢复耗时,才算完成闭环。
假设在线学习平台有三类 Redis 数据:
一种稳妥的演进路线是:
先治理慢命令、big key 与热点,建立按业务命令比例压测的单机基线。课程详情可以加应用本地短缓存,先削掉最热读取。
在容量仍能放进单节点时,为 primary 配置跨故障域 replica,并用 Sentinel 做自动故障转移。课程详情可读 replica,学习进度写后读和会话校验仍走当前 primary。
依据峰值复制字节速率与目标断线时间配置 backlog,监控复制偏移;对会话写评估 WAIT 或最小 replica 写入限制,明确它们只缩小风险窗口。
写吞吐或内存接近安全线时迁移到 Cluster。以学生 ID 设计 与 ,让同一学生的原子操作同槽,同时让不同学生分散到不同槽。
这条路线没有把 Redis 描述成银弹。副本读会陈旧,Sentinel 仍有异步复制窗口,Cluster 会限制跨槽操作,托管服务也有产品边界。好的扩展设计不追求“没有代价”,而是让代价在上线前就被看见、被监控、被演练。
最后一道开放题:为什么“有 replica”仍不等于“数据不会丢”?
当你能先用指标区分慢命令、热点、读瓶颈、写瓶颈和容量瓶颈,再说明复制窗口、Sentinel 投票、Cluster 槽路由与迁移回滚,你掌握的就不只是“怎样加节点”,而是一套能解释失败、能验证恢复的 Redis 扩展方法。
Sentinel 更新并持久化自己的配置,客户端通过 Sentinel 服务发现得到新 primary 地址。
旧 primary 恢复后会被重新配置为 replica,而不是自动抢回 primary 角色。
对照恢复时间、错误率和可能的数据差异,更新超时、重试、告警和操作手册。
progress:{studentId}:summaryprogress:{studentId}:last上线前验证客户端支持 CLUSTER SHARDS、MOVED、ASK、有限重试和连接池刷新,再用少量冷槽演练在线迁移。
最后演练单分片故障、少数派网络隔离、replica 落后、backlog 不足和备份恢复,把实际 RTO/RPO 写回手册。