1. 为什么Redis Cluster非要走分片这条路
先回答一个很多刚接触Redis Cluster的人都会问的问题:单机Redis明明够用,为什么要搞出“数据分片”这一套?
原因其实很现实。单机Redis的内存上限、CPU处理能力、网络带宽都有天花板。假设你有一台32GB内存的物理机,跑着Redis作为缓存,给业务扛每天几亿次的读请求。一开始很爽,但业务量翻倍、数据量翻倍之后,这台机器的内存被打满,CPU跑到80%以上,延迟开始抖动。这时候你有两个选择:换一台更大内存的机器,或者把数据拆开,放到多台机器上。前者叫垂直扩容,后者叫水平扩容。
垂直扩容看起来简单,但有两个致命问题。第一,单机内存再大有上限,32GB不够你换64GB,64GB不够你换128GB,可128GB内存的机器价格昂贵,而且当内存超过一定规模后运维成本会急剧上升。第二,单台机器承载所有流量,一旦挂了就是全量故障。Redis Cluster走的是水平扩容的路子,它把数据切碎,分散到多个节点上,每个节点只负责一部分数据,既解决了单机压力问题,又天然实现了分布式的容灾。
数据分片机制就是Redis Cluster的核心血管。它决定了你的key会被存到哪台节点上、读写请求怎么路由、节点扩容缩容时数据怎么搬。如果你只是用Redis单机做缓存,可能感受不到分片的存在。一旦你上了Redis Cluster,分片机制会在每一个读写操作中生效,而且你不需要在业务代码里手动算这个key该去哪台机,Redis Cluster的集群协议把这件事自动处理掉了。
这个过程可以打个比方:像一个图书馆把所有书按编号分成了一万个格子,每层楼负责其中一部分格子,你拿书号去查,管理员看一眼就知道去几楼拿。Redis Cluster的槽(slot)就是这个“格子”,所以社区里也常称它“哈希槽分片”。
这篇文章就把Redis Cluster的分片机制完整拆开,讲清楚背后的原理和实际操作中会遇到的坑。适合已经在用Redis、正准备上Cluster,或者已经在Cluster上踩过坑又没时间系统研究的同学。
2. 哈希槽:数据分片机制的核心算法
2.1 16384这个数字是怎么来的
Redis Cluster没有用一致性哈希,而是自己设计了一套槽(slot)机制。整个集群固定划分为16384个哈希槽,这个数字可能有人觉得随意,其实是在工程实践中权衡出来的。
每个节点启动后,会把自己负责的槽位信息广播给集群内的其他节点,节点之间靠心跳包维持元数据同步。心跳包既要携带槽位信息,又要控制体积,否则集群节点多了,光是同步元数据就会把内网带宽吃掉。16384个槽位,用bitmap表示只需要2KB(16384 / 8 = 2048字节)。这个体积放在Gossip协议的心跳包里非常合适,既足够细分数据粒度,又不会让心跳包过大。
如果槽位数太少,数据分布会粗糙,扩缩容时迁移粒度太大——比如只有1024个槽,一次迁移就可能搬走大量key。如果槽位数太多,比如65536个槽,bitmap就要占用8KB,节点之间传递消息的开销翻四倍,单条心跳消息可能被撑爆。而且主节点数量超过1000个后,Gossip协议的同步成本会指数级上升,16384这个值恰好能在常见规模(几十到几百个节点)下保持元数据同步高效。
从最终实践来看,16384是Redis官方在数据规模、通信开销和实现复杂度三者之间找到的平衡点。作为使用方,不需要改这个数字,但理解它有助于你理解后续的迁移、路由机制。
2.2 key是怎么落到槽上的
当你向Redis Cluster发送一条命令,最终会执行以下三步。
第一步,对key做哈希计算。Redis使用的是CRC16算法,不是简单的取模。CRC16(Cyclic Redundancy Check,循环冗余校验)输入一个key,输出一个16位的值,范围在0到65535之间。
第二步,对这个哈希值做取模运算:slot = CRC16(key) % 16384。这一步把CRC16的输出映射到0到16383的槽位上。
第三步,根据槽位找到对应节点,把命令发过去执行。如果客户端已经缓存了槽和节点的映射关系,它会在本地就能定位到目标节点,直接发送请求,不需要经过中间层转发。
这三个步骤里,可能你觉得等价的,攒一个“精确查表”的应用:与其在客户端自己实现一套路由,不如直接依赖Redis Cluster的槽机制。实际上官方库已经帮你封装好了,你只要用支持Cluster的客户端即可。
这里可以做一个简单的验证。假设key是“user:10001”,先用redis-cli的cluster keyslot命令可以直接查看这个key对应的槽位和节点。我在实际测试环境里跑过:
redis-cli -c -h 127.0.0.1 -p 7000 cluster keyslot user:10001返回的槽位编号就是一个整数,例如 12345。然后你可以再用cluster nodes命令查看这个槽位在哪个主节点上。这个命令实操中排查key分布问题时特别有用。
2.3 hash tag:把多个key锁进同一个槽
分片机制有个天然限制:一个多key操作,比如MGET、MSET、SUNIONSTORE,如果需要操作的key落在不同的槽上,普通模式下这些操作会直接报错(CROSSSLOT)。这在业务中很常见,例如批量查询用户信息时用MGET,涉及的userId不同,算出来的槽位自然可能不同。
Redis提供了一个讨巧的机制,叫hash tag。规则很简单:如果key里包含花括号{},只有花括号内的部分参与CRC16计算。举个例子:
- key“user:{10001}:profile”,花括号里是10001,只对10001做哈希
- key“user:{10001}:orders”,花括号里同样是10001,哈希值和上面完全相同
这两个key必然落在同一个槽位上,因此可以安全地放在同一个MGET命令中执行。
hash tag的使用有一个重要经验:不要在key里乱加花括号,否则会把本来应该分散的数据挤到同一个槽位上,造成数据倾斜。比如把所有用户都设计成“user:{common}:profile”,那就意味着所有用户的数据都落在同一个槽位上,其他节点闲着,一个节点被打爆。hash tag只适合用在确实需要跨key原子操作的场景,而且要保证花括号内部的值有足够的区分度。
3. 槽位分配与节点架构
3.1 槽是怎么分配出去的
当一个Redis Cluster集群初始化的时候,16384个槽一个都没有归属。你需要手动执行cluster addslots命令,把槽分配给各个主节点,或者使用redis-cli --cluster create命令让工具自动平均分配。
以一个三主三从的典型集群为例,三个主节点分别会分到大致等量的槽位。常见做法是每个主节点分到5461或5462个槽,三节点加起来正好16384。从节点不分配槽,它们只是主节点的副本,主节点挂了从节点顶上。
分配槽位并不需要平均,你可以按节点性能手动调整。例如机器A更强,你给10000个槽,机器B弱一些,给3184个槽。不过日常不建议这么干,除非你对机器配置差异非常清楚,否则手动分配容易让后续扩缩容的逻辑变复杂。
槽位的分配信息在集群里是全局一致的。每个节点都维护着一张槽位映射表,记录着“哪个slot由哪个主节点负责”。节点之间通过Gossip协议持续交换这份信息,保证集群内所有节点对数据归属的理解一致。客户端不需要自己维护这张表,它向任意节点发起命令时,节点会返回正确的路由信息,客户端再缓存下来。
3.2 一次读取请求的完整路径
用一个具体的GET命令来梳理数据分片机制下的完整请求路径。
假设集群有三个主节点A、B、C,客户端要执行GET user:10001。步骤如下:
第一步,客户端根据本地缓存的槽位映射表,计算CRC16(key) % 16384,得到槽位编号。第二步,查映射表,判断这个槽位属于哪个主节点。如果本地缓存有这段信息且没有过期,直接跳到第四步。第三步,如果客户端还没建立完整的映射缓存(比如刚启动闲着没事),它可以向任意节点发送cluster slots命令,拉取全量槽位映射信息,然后在本地建立缓存。第四步,把GET命令直接发给目标主节点,目标节点执行并返回结果。
这里有一个关键细节:节点之间不会转发读写命令。每个节点只处理落在自己槽位上的key,“我没管这个槽”的情况处理方式是节点直接返回MOVED错误,而不是帮你转发。MOVED错误里包含目标节点的IP和端口,客户端拿到MOVED后更新本地缓存,再往目标节点发起真正的请求。
你可以把MOVED理解成一个“走错门”的提示:你去了A节点,A说这个key归B管,你拿着B的地址重新去B。第一次请求多了一次网络往返,之后缓存更新了就不会再错。这个设计保证了节点间不依赖转发,每个节点的压力是可控的,不会出现“前面节点忙得要死,后面节点闲着”的转发瓶颈。
3.3 数据分片下的Redis Cluster是否真的平均
理论上哈希槽可以保证数据均匀分布,但实际使用经常会出现数据倾斜。原因主要有几个。
第一,key分布不均匀。比如没有使用hash tag,但业务key本来就有明显的冷热分布,某些key被访问的频率远高于其他key,导致某个节点CPU使用率偏高。
第二,hash tag使用不当。前面说的把大量不同业务数据强制挤进同一个花括号里,就会造成单个槽位数据量极大。一个槽位的数据可能包含了数以百万计的key,而这个槽位只由一个主节点负责,其他节点都在看戏。
第三,数据删除和过期策略导致某些槽位数据被清理得多,某些清理得少,长期运行后各节点的内存水位不一样。虽然Redis Cluster没有自动再平衡的功能,但你可以在业务低峰期手动执cluster rebalance(老版本叫--cluster rebalance)来重新分配槽位,让数据重新均匀化。
我个人的建议是,在架构设计阶段就把key的粒度设计好。使用业务前缀区分不同业务,但不轻易加花括号。比如“order:20240901:12345”“user:10001”,算出来的槽位天然分散。只有在需要事务性多key操作时才考虑hash tag,并且tag内的值要有足够的基数。
4. 数据迁移:分片机制里的关键手术
4.1 迁移的完整流程
Redis Cluster支持在线扩容、缩容和槽位重分配,这三个操作本质上都涉及数据迁移。数据迁移的最小单位是槽位,一个槽位迁移完成后,该槽下的所有key才会迁移到新节点。
以扩容为例,加入一个新节点D后,D一开始没有槽位。你需要从A、B、C各匀一部分槽给D。执行redis-cli --cluster reshard 127.0.0.1:7000,它会进入交互式向导,让你输入要迁移多少槽、迁移到哪个节点ID、从哪些源节点迁。实际操作中这个向导可以自动化,用脚本传入参数就能跑。
迁移单个槽位的过程中,涉及源节点和目标节点之间协作。假设要把槽1000从A节点迁移到D节点,流程如下:
第一步,目标节点D执行cluster setslot 1000 importing A_ID,表明自己正在导入这个槽。第二步,源节点A执行cluster setslot 1000 migrating D_ID,表明自己正在迁出这个槽。第三步,把槽1000内的key分批从A抓到D。抓取命令是cluster getkeysinslot,先取出这个槽里的所有key,再逐个执行MIGRATE命令把key及value搬到D。第四步,所有key迁移完成后,A执行cluster setslot 1000 node D_ID,把槽1000的归属权移交给D,同时这个消息会通过Gossip广播到整个集群。
这个过程中有一个数据不一致的风险点:如果客户端在迁移过程中写入key 1000,而这个key对应的数据已经被搬到D节点,A节点上就查不到该key,写入会怎样?
Redis Cluster的处理方案确实很巧妙。如果key还没搬走,A正常处理写入。如果key已经搬走了,A会返回MOVED或者ASK错误,客户端根据错误信息去D节点操作。如果在MIGRATE搬运的半途中写入,A会返回TRYAGAIN,意思是“正在迁移,你等一下再试”。
实际运维中,迁移期间应用可能会看到少量的TRYAGAIN错误。这通常不会导致业务故障,只要客户端有重试机制即可。但如果你用的是不带重试功能的客户端,就要在迁移期间格外关注错误率。
4.2 迁移速度怎么控制
MIGRATE批量搬key的时候,有一个参数要先弄清楚:batch size。redis-cli --cluster reshard默认一次搬多少key?实际上是逐条执行的。如果槽位里key数量巨大(几百万),一次迁移需要非常长的时间。
推荐的做法是使用--cluster reshard的--cluster-from、--cluster-to、--cluster-slots参数指定范围,并且配合--cluster-yes跳过交互确认。更精细化地,你可以用redis-cli --cluster reshard的timeout和pipeline参数控制每次迁移的批量和超时时间。
这里分享一个我在实际迁移中常用的脚本思路:
redis-cli --cluster reshard $HOST:$PORT \ --cluster-from $SOURCE_NODE_ID \ --cluster-to $TARGET_NODE_ID \ --cluster-slots 1000 \ --cluster-yes \ --cluster-timeout 5000 \ --cluster-pipeline 10--cluster-pipeline 10的意思是每批用pipeline方式发送10个MIGRATE命令。这样可以在不把网络打爆的前提下,把迁移速度调到可控范围内。迁移过程中监控源节点和目标节点的内存变化,对比和总数据量的差值,就能估算迁移进度。
4.3 扩容缩容时的分片变化对客户端的影响
节点变化后,槽位映射表发生了变化。客户端需要感知到这个变化并更新本地缓存。怎么感知?
两个途径。第一,当客户端向某个节点发请求收到MOVED错误时,客户端会更新本地的槽位映射缓存。第二,客户端可以定期向任意节点拉取cluster slots信息,主动刷新缓存。大部分官方客户端(如Redis的Java客户端Lettuce、Python客户端redis-py-cluster)内置了自动刷新机制,但在节点变化瞬间,仍然可能出现少量路由错误。
这里有一个常见的误解:扩容之后,新节点还没有任何槽位,它不会接收任何读写请求。你会看到新节点起来后CPU占用几乎为0,内存占用也几乎为0,这时候如果业务方说“扩容没效果”,是对的。只有当槽位迁移过去了,新节点才开始承担压力。所以扩容过程中,数据不是“自动分布”的,必须手动执行reshard,很多人这里踩坑。
缩容过程正好相反。先把要下线的节点上的槽位迁走,迁移完成后再cluster forget下线该节点,最后停掉进程。如果你不迁移槽位就直接停节点,那么这些槽位对应的key将全部不可用,整个集群会进入部分失败状态。
5. 分片机制下的机房容灾与副本设计
5.1 主从副本怎么和分片配合
每个主节点可以配置一个或多个从节点。从节点不参与分片,不承担写操作,但它们承担两个任务:一是数据备份,二是主节点故障时顶替主节点。从节点通过主从复制机制同步主节点上的全部数据。
这里注意:从节点同步的是它对应主节点负责的槽位数据,而不是整个集群的数据。每个主节点都只存了全量数据的一部分,从节点负责复制这一部分,所以整个集群的副本总量是“全量数据 x 副本数”。三主三从的集群,实际存储了全量数据的两份拷贝。
一个误区是很多人以为Redis Cluster天然会保证每个主节点和从节点不在同一台物理机上。实际上默认并不会,需要你手动规划。如果A主节点和它的从节点落在同一台物理机,这台物理机宕机,主从同时挂,这个分片的可用性就没了。
生产环境一定要用master和replica的节点ID配合--cluster-replicas参数在创建集群时指定副本数,并确保同一分片的主从节点分布在不同的机架或可用区。云上部署时,更是要把主从节点放到不同的可用区,才能扛住单可用区故障。
5.2 集群脑裂与分片可用性
Redis Cluster在节点通信上采用Gossip协议,节点间通过PING/PONG消息维持心跳。如果由于网络分区,一部分节点无法和另一部分节点通信,集群可能会发生脑裂。
Redis Cluster的脑裂处理机制是:集群中大多数主节点参与投票,当某个主节点无法与大多数主节点通信时,它会被标记为主观下线,随后被标记为客观下线,其他主节点会选举该分片的从节点晋升为新主节点。
这个过程对你的业务有什么影响?如果你使用默认配置,并且分片的主从在同一个网络分区内,故障切换可以自动完成。但如果主从也被网络分区隔开,则可能出现没有从节点可以晋升的情况,这个分片的数据读写就会中断。
实际操作中,我建议给每个分片至少配置一个从节点,并且将cluster-node-timeout参数设置为合适的值。这个参数默认是15000毫秒,如果设置太小,网络抖动容易触发误切换;设置太大,主节点宕机后故障切换时间过长。常见推荐值是10到30秒。云环境网络波动较大,我会倾向于用20秒左右,让短暂抖动自己恢复,避免频繁切换副本影响性能。
6. 分片机制实操中的常见问题与排查实录
6.1 常见问题速查表
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| 客户端报MOVED错误 | 客户端槽位缓存过期 | 检查客户端版本,确保支持Cluster自动路由;重启客户端或等待缓存刷新 |
| 单节点数据量比预期大 | hash tag滥用导致数据倾斜 | 用cluster keyslot命令检查热点key,分析key分布 |
| 迁移期间大量TRYAGAIN | MIGRATE过程中请求频繁 | 确认迁移是否在进行中;业务侧增加重试 |
| 新节点加入后无流量 | 未执行reshard分配槽位 | 用cluster nodes确认新节点槽位 |
| 从节点内存不断上涨 | 从节点暂存复制缓冲 | 检查全量重同步是否频繁触发,调整repl-backlog-size |
| 集群中某个分片延迟特别高 | 该分片槽位数据量大或者访问热 | 使用CLUSTER COUNTKEYSINSLOT检查槽位key数量,使用INFO命令确认节点CPU和内存 |
这张表里的内容都是我排查线上问题时遇到过的真实情况,不是教科书式的举例。尤其是第一行MOVED错误,很多新手一开始以为MOVED是故障,其实它只是正常的路由通知。
6.2 一次实际的故障排查记录
有一次我维护的Redis Cluster集群,某个主节点CPU飙升到90%,其他节点只有30%左右。看起来像数据倾斜,但checked后发现槽位数量是均衡的,每个主节点的key数量也差不多。
用redis-cli --cluster info确认集群各节点的内存情况后,进一步用monitor命令观察热点,发现大量读请求集中在一批固定key上,这些key全部集中在这个节点负责的某几个槽位里。数据量不多,但是访问频率极高,直接把CPU打满。
这就是典型的“访问倾斜”而不是“数据倾斜”。处理方法是把热点key更细粒度拆散,比如在key后面拼接随机后缀,让这些热点请求分散到不同的槽位和节点上。如果你不想改业务代码,临时方案是在热点key前面加多级缓存,把压力挡在Redis之前。
6.3 分片机制下的选型经验:什么时候不该用Cluster
这里想给一个反向建议。Redis Cluster不是银弹,如果你的数据量小于20GB,单节点内存完全够用,并且没有写并发瓶颈,不建议直接上Cluster。原因有三点。
第一,Cluster带来了额外的运维复杂度,扩缩容、槽迁移、故障转移、监控告警都要额外维护。第二,多key操作受限,很多简单的MGET跨槽就做不了,要么加hash tag,要么改成多次单点查询,这本身就是对业务的一种约束。第三,client端兼容性,虽然官方客户端都支持了,但仍有一些老客户端和第三方工具对Cluster支持不完善。
反过来,如果你的数据量会超过单机内存,或者写流量会超过单节点CPU处理能力,或者你本身就是多机房、多可用区的部署要求,那么Cluster是比较合理的选择。到时候你再回头读这篇文章,里面的坑就都能对号入座了。
我个人的经验是:上Cluster之前先在测试环境完整跑一遍扩容、迁移、故障转移的演练。特别是业务低峰期真正跑一次数据搬迁,把迁移耗时、对QPS的影响、报错情况都记录下来。这样线上真的需要扩容时,你有数据参考,心里有底,不会手忙脚乱。