
1. 为什么Redis Cluster非要走分片这条路先回答一个很多刚接触Redis Cluster的人都会问的问题单机Redis明明够用为什么要搞出“数据分片”这一套原因其实很现实。单机Redis的内存上限、CPU处理能力、网络带宽都有天花板。假设你有一台32GB内存的物理机跑着Redis作为缓存给业务扛每天几亿次的读请求。一开始很爽但业务量翻倍、数据量翻倍之后这台机器的内存被打满CPU跑到80%以上延迟开始抖动。这时候你有两个选择换一台更大内存的机器或者把数据拆开放到多台机器上。前者叫垂直扩容后者叫水平扩容。垂直扩容看起来简单但有两个致命问题。第一单机内存再大有上限32GB不够你换64GB64GB不够你换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表示只需要2KB16384 / 8 2048字节。这个体积放在Gossip协议的心跳包里非常合适既足够细分数据粒度又不会让心跳包过大。如果槽位数太少数据分布会粗糙扩缩容时迁移粒度太大——比如只有1024个槽一次迁移就可能搬走大量key。如果槽位数太多比如65536个槽bitmap就要占用8KB节点之间传递消息的开销翻四倍单条心跳消息可能被撑爆。而且主节点数量超过1000个后Gossip协议的同步成本会指数级上升16384这个值恰好能在常见规模几十到几百个节点下保持元数据同步高效。从最终实践来看16384是Redis官方在数据规模、通信开销和实现复杂度三者之间找到的平衡点。作为使用方不需要改这个数字但理解它有助于你理解后续的迁移、路由机制。2.2 key是怎么落到槽上的当你向Redis Cluster发送一条命令最终会执行以下三步。第一步对key做哈希计算。Redis使用的是CRC16算法不是简单的取模。CRC16Cyclic 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分布迁移期间大量TRYAGAINMIGRATE过程中请求频繁确认迁移是否在进行中业务侧增加重试新节点加入后无流量未执行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的影响、报错情况都记录下来。这样线上真的需要扩容时你有数据参考心里有底不会手忙脚乱。