读完这篇,你能做什么
能规划同一业务原子操作的键,并解释分片扩容和故障切换的不同作用。
学习理由、准备要求与知识点
为什么学它
集群改变键的路由和多键操作边界,单机正确的 Lua 或事务不能直接假设集群可用。
先读这些内容
知识点清单
- 16384 哈希槽
- 节点与分片
- MOVED 与 ASK
- 集群客户端
- 哈希标签
- 同槽多键操作
- 迁槽
- Sentinel 与 Cluster
运行前先确认Redis Cluster 7.4;源码固定 Jedis 5.2.0。Java 17 完整程序仅计算字节哈希与批次槽分组,不建立集群。CLUSTER 命令需要独立 Cluster 环境,普通单机实例不适用。
01先弄懂它是什么
数据或请求太多时,分片把负担分到多个节点;副本则复制数据,并在部分故障时接管,二者用途不同。Redis Cluster 先把键分到 0 至 16383 这些哈希槽,再把槽分配给主节点。增加节点改变的是哪个节点负责哪些槽,不是键本身的槽号,所以客户端要根据槽找节点,不能直接按节点数量给键取模。
需要一起原子处理的多个键,设计时就应安排在同一个槽。例如 Lua 要同时改优惠券库存和购买者集合,这些键必须满足同槽要求。它们目前碰巧在同一台主节点上并不够,因为迁移槽后可能分开;不同槽也不会因为暂时同节点,就满足标准多键操作的同槽限制。
哈希标签用来让需要一起操作的键进入同一个槽,设计时先问:最少有哪些数据必须一起改?例如同一张优惠券的库存和购买者集合,可以按券编号安排。若所有业务都用一个标签,请求和内存就会挤在同一个槽;若只按用户分,又可能把必须一起处理的券库存拆开。要同时考虑哪些操作必须一起完成、流量能否分散,以及以后怎样迁移数据。
文中这几个词,先认一认
- 哈希槽
- Redis Cluster 给键计算的逻辑编号,范围为 0 到 16383,再由这个编号找到负责节点。
- 哈希标签
- 键名中符合规则的一段花括号内容,用这部分计算槽,便于让相关键落在一起。
- MOVED 与 ASK
- MOVED 告诉客户端槽应找哪个节点;ASK 用于迁移期间,让本次请求临时转向另一个节点。
02看看它怎么工作
先按实际发送字节理解哈希。使用 CRC-16/XMODEM 后取低十四位得到槽号;不是 Java String.hashCode,也不是对 Unicode 字符逐个直接运算。键包含花括号时,从第一个左括号寻找其后的第一个右括号,只有中间非空才使用标签。foo{}{bar} 使用整个键,不能跳过空标签继续选择 bar。算法中的这些边界会决定应用与服务端是否算出相同路由。
在隔离 Cluster 中执行 CLUSTER KEYSLOT stock:{voucher-7} 与 CLUSTER KEYSLOT buyers:{voucher-7},比较输出,再换成 voucher-8。redis-cli -c 能跟随重定向,应用则应采用 Cluster 客户端并配置可从应用网络到达的节点地址。连接种子节点成功只是启动入口可达,不能证明拓扑返回的所有地址都可达。
迁槽时客户端可能收到 ASK,需要在目标连接发送 ASKING 后重试这次请求,而不是立即永久改写该槽映射;MOVED 则提示更新归属或刷新拓扑。同槽多键在迁移过程中也可能暂时不可执行,因此同槽是必要设计条件,不是任何时刻成功的保证。结果未知的网络超时与明确重定向应分类处理,不能统统盲目重放非幂等命令。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
A["键的实际编码字节"] --> B["选择有效哈希标签或完整键"]
B --> C["CRC16 取低十四位"]
C --> D["查槽到节点映射"]
D --> E["执行请求"]
E --> F["MOVED 刷新映射"]
E --> G["ASK 临时 ASKING 后重试"]按顺序看,每一步做了什么
- 列出需要原子处理的键集合,选择最小业务标签。
- 用字节算法和 CLUSTER KEYSLOT 比较同槽关系。
- 验证客户端拓扑发现、节点可达与重定向处理。
- 在迁槽和故障演练中观察超时、热点及多键失败策略。
03对照实现代码
Jedis v5.2.0 · JedisClusterCRC16#getSlot(byte[]);JedisClusterCRC16#getCRC16(byte[],int,int)
下面是原创标签选择教学实现。真实 Jedis 使用 CRC 查表法,完整算法部分改用更易推导的逐位计算;两者不属于 Redis 服务端实现。
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
class SourceModel {
static byte[] hashInput(String key) {
byte[] bytes = key.getBytes(StandardCharsets.UTF_8); // 显式统一网络编码
int open = -1;
for (int i = 0; i < bytes.length; i++) {
if (bytes[i] == '{') { open = i; break; } // 只选择第一个左括号
}
if (open >= 0) {
for (int end = open + 1; end < bytes.length; end++) {
if (bytes[end] == '}') {
if (end == open + 1) return bytes; // 空标签直接退回完整键
return Arrays.copyOfRange(bytes, open + 1, end);
}
}
}
return bytes; // 缺少配对右括号也使用完整键
}
}真实 byte[] 重载记录第一个左括号和随后第一个右括号,仅在两者之间存在字节时选择子区间。该规则不支持任意挑选最后一组括号。
CRC 方法每次结合前一状态和当前无符号字节查表,最终限制为十六位;槽计算再通过 16383 掩码取得十四位。
String 重载先经过标签处理与编码,二进制键则直接处理字节。教学实现固定 UTF-8 字符串入口,不覆盖任意二进制键的输入解析。
这段源码只计算键属于哪个槽,不决定槽由谁负责、故障时选谁为主,也不保证数据保存。槽号算对,只完成了本地路由计算,还不能证明集群请求已成功。
代码先调用谁,再调用谁
- 业务键编码
- 有效标签选择
getCRC16- 低十四位得到 slot
- 客户端查询拓扑并发出命令
04动手写一个例子
逐位 CRC16 与同槽批次分组
对 UTF-8 键按 Redis 标签规则计算槽,将请求按槽分组,并检查一个要求原子的键集合是否同槽。分组只生成执行计划,不会把多个槽变成一个事务。
- 输入 1
a{123456789}、b{123456789}- 输出 1
两个键均落在槽12739
- 输入 2
foo{}{bar}- 输出 2
以完整键计算,不能取bar
先选择参与计算的字节,避免把键名中不生效的花括号误认为标签。
CRC 初值为零,将每个无符号字节放入高八位,逐位左移;最高位为一时异或多项式 0x1021,始终保留十六位。
计算完成后与 0x3fff 做按位与,分组使用槽号作为键。不同标签可能碰撞到同槽,因此判断应比较槽号而不是比较标签字符串。
为什么这样写是对的处理每一位后 crc 始终位于十六位范围,并表示截至该位的 XMODEM 余数;输出槽始终位于零到16383。
B 为所有键的编码字节总量,每字节固定八步;存储编码或标签副本及 n 个分组引用。若逐键流式处理,临时字节空间可降到最长键长度。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.nio.charset.StandardCharsets;
import java.util.*;
public class Main {
static byte[] input(String key) {
byte[] b = key.getBytes(StandardCharsets.UTF_8);
int left = -1;
for (int i=0; i<b.length; i++) if (b[i]=='{') { left=i; break; }
if (left >= 0) {
for (int i=left+1; i<b.length; i++) if (b[i]=='}') {
return i == left+1 ? b : Arrays.copyOfRange(b,left+1,i); // 空标签不继续寻找
}
}
return b;
}
static int crc(byte[] bytes) {
int value = 0;
for (byte b : bytes) {
value ^= (b & 0xff) << 8; // byte 有符号,先还原为无符号八位数
for (int bit=0; bit<8; bit++)
value = ((value & 0x8000) != 0 ? (value << 1) ^ 0x1021 : value << 1) & 0xffff;
}
return value; // 每步掩码保持十六位余数
}
static int slot(String key) { return crc(input(key)) & 0x3fff; }
static Map<Integer,List<String>> group(List<String> keys) {
Map<Integer,List<String>> result = new TreeMap<>();
for (String key : keys) result.computeIfAbsent(slot(key), s -> new ArrayList<>()).add(key);
return result; // 只是独立槽批次,不能据此执行跨槽原子操作
}
static int requireSameSlot(List<String> keys) {
if (keys.isEmpty()) throw new IllegalArgumentException("empty keys");
int expected = slot(keys.get(0));
for (String key : keys) if (slot(key) != expected) throw new IllegalArgumentException("CROSSSLOT");
return expected;
}
static void check(boolean ok) { if (!ok) throw new AssertionError(); }
static String selected(String key) { return new String(input(key), StandardCharsets.UTF_8); }
public static void main(String[] args) {
check(crc("123456789".getBytes(StandardCharsets.US_ASCII)) == 0x31c3);
check(slot("") == 0);
check(requireSameSlot(List.of("a{123456789}","b{123456789}")) == 12739);
check(selected("foo{}{bar}").equals("foo{}{bar}"));
check(selected("foo{{bar}}zap").equals("{bar"));
check(selected("foo{bar}{zap}").equals("bar"));
check(selected("foo{bar").equals("foo{bar"));
check(selected("stock:{优惠券7}").equals("优惠券7"));
check(group(List.of("a{123456789}","b{123456789}","")).size()==2);
boolean rejected=false;
try { requireSameSlot(List.of("a{123456789}","")); }
catch (IllegalArgumentException expected) { rejected=true; }
check(rejected);
System.out.println("10 cluster slot checks passed");
}
}这些特殊情况也要试一试
- 空标签和缺少右括号时使用整个键。
- 嵌套括号按字节位置处理,不做语法嵌套解析。
- 槽碰撞正常存在,同槽不表示属于同一业务或具有授权关系。
进阶选读 · 生产拓展:在热点约束下选择标签粒度
场景一张热门券的全部请求集中到一个槽,增加其他主节点也不能分散该原子热点。
做法先度量按标签的请求率和数据体积,再考虑业务分桶与预分配库存;每桶保持局部原子性,跨桶汇总、调拨和防超卖另建协议,不能只在键名追加随机数。
复杂度观测统计随请求量增长;本地分桶路由可为 O(1),跨桶一致性成本由业务协议决定。
好处与代价分桶有助于并行,但会引入库存分配不均和跨桶协调,可能不值得为普通负载采用。
05项目里怎么用
迁到 Cluster 前,列出所有多键命令、Lua、事务和程序拼出的键,逐项计算它们是否同槽。脚本使用的键要按客户端和服务端要求明确传入,别在脚本里临时拼跨槽键。某些客户端能把 MGET 拆成多个请求,但这时各部分不再保证来自同一个瞬间的整体快照。
扩容的工作包含分配槽、迁移数据、验证地址可达及观察流量倾斜。槽数量均匀不代表内存或负载均匀,一个超大键和一个热门标签就可能主导节点成本。迁移大键会影响延迟,应该用实际键体积与负载评估窗口,而不是只按槽数平均分配。
副本可能读到旧数据,异步复制和网络问题也可能让切换后的节点丢掉部分写入。关键业务仍要保留数据库记录、重复请求处理和对账能力,才有办法确认不确定结果。还要设结果查询入口及重试上限,避免请求反复重定向或大量客户端一起刷新节点信息造成新高峰。
容易踩的坑
- 用 Java
hashCode或节点数量取模代替 Redis 槽算法。 - 认为同节点就满足同槽原子操作条件。
- 遇到 ASK 就永久更新槽映射,忽略一次性 ASKING 语义。
- 把所有业务都放进同一个标签,使集群退化成热点单槽。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 按业务聚合根使用标签 | 容易形成清晰的局部原子范围 | 热门聚合可能集中负载 | 单券、单账户等有明确边界的状态 |
| 按槽拆分批量请求 | 可利用多个节点并行 | 没有跨槽原子快照与统一提交 | 允许独立读取或部分失败的批处理 |
| 读取副本 | 分担部分读负载 | 需要接受复制延迟和陈旧读取 | 不要求立即读到新值的页面 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01扩容后键的槽号会变化吗?
展开参考答案与推导
相同字节与标签规则下槽号不变,改变的是槽到主节点的映射。
02同槽是否等于同业务?
展开参考答案与推导
不是。哈希有碰撞,不同业务键也可能落同槽;业务边界由应用定义。
03Sentinel 与 Cluster 的主要区别是什么?
展开参考答案与推导
Sentinel 主要处理监控与故障切换,Cluster 还提供基于槽的数据分片;不能将 Sentinel 当成自动分片。
换个情况,还会处理吗
01种子节点能连,但读取某些键失败,先检查什么?
展开参考答案与推导
检查拓扑公布的目标节点地址是否可达、认证是否一致,以及客户端是否支持重定向。
02同槽脚本迁移期间偶发失败,能证明设计错误吗?
展开参考答案与推导
不能直接证明。同槽迁移期间仍有暂时不可用情况,要结合错误类型、迁移状态和有界重试观察。
03十个节点中一个持续高负载,平均槽数正常,怎么办?
展开参考答案与推导
按键和标签检查热点与体积;节点或槽数量均匀不等于请求与内存均匀。
写代码试一试
01foo{}{bar} 使用哪个哈希输入?
- 输入 / 条件
foo{}{bar}- 输出 / 预期
完整键
提示 第一组为空即停止。
展开参考答案与推导
不能继续寻找 bar;返回整个编码字节数组计算 CRC。
02foo{{bar}}zap 使用哪个片段?
- 输入 / 条件
foo{{bar}}zap- 输出 / 预期
{bar
提示 第一个左括号到其后第一个右括号。
展开参考答案与推导
不处理嵌套语义,所以片段包含第二个左括号。
03标准向量123456789的槽号是多少?
- 输入 / 条件
CRC为0x31C3- 输出 / 预期
12739
提示 取低十四位。
展开参考答案与推导
0x31C3 小于0x4000,与0x3fff后不变,十进制为12739。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能规划同一业务原子操作的键,并解释分片扩容和故障切换的不同作用。
本篇重点加练
重定向和同槽约束在迁移期间同时生效,需明确重试何时会涉及业务结果未知。
- MOVED 与 ASK
- 同槽多键操作
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
三节点拥有不同槽区间,而一个活动键比其他键热百倍。画出键、槽、节点映射,解释均匀槽数能否代表均匀负载。
- 提交内容
- 映射图与负载假设
- 怎样算完成
- 保持槽和节点概念独立
- 包含热点与数据量两种不均衡
练到的知识点16384 哈希槽 · 节点与分片
- 作业 02代码推演
客户端缓存了旧路由,连续收到 MOVED 与 ASK 两种响应。分别记录后续连接动作和路由表该保存的信息。
- 提交内容
- 客户端状态图
- 怎样算完成
- 区分临时和持续路由信息
- 重试有次数与时间上限
练到的知识点MOVED 与 ASK · 集群客户端
- 作业 03设计评审
购物车结算要同时操作用户购物车和活动库存。设计两套哈希标签方案,评审多键操作适用范围及热点风险。
- 提交内容
- 键样本与操作矩阵
- 怎样算完成
- 逐键核查槽一致性
- 不只追求所有键落同槽
练到的知识点哈希标签 · 同槽多键操作
- 作业 04代码推演
客户端连续得到重定向和连接中断,上一条写命令可能已执行。分别记录路由纠正与业务重试的判断依据。
- 提交内容
- 请求身份与路由时间线
- 怎样算完成
- 区分尚未发送和已发送未确认
- 有明确总重试预算
练到的知识点MOVED 与 ASK
综合应用
- 作业 05面试场景
现有单主多副本部署需要自动切换,团队又希望扩展总数据量。比较 Sentinel 与 Cluster 的目标,并讨论迁槽期客户端需要观察的异常。
- 提交内容
- 需求映射与迁移风险表
- 怎样算完成
- 区分高可用与分片
- 注明操作期间的可用性假设
练到的知识点迁槽 · Sentinel 与 Cluster
- 作业 06编程实践
实现本地 Java 键路由演示器,输入键与节点槽表,输出路由及多键兼容报告;另用本地客户端桩测试重定向次数上限。
- 提交内容
- 代码、样本与校验记录
- 怎样算完成
- 包含 Unicode 和多组花括号
- 声明不验证真实迁槽与故障切换
练到的知识点16384 哈希槽 · 节点与分片 · MOVED 与 ASK · 集群客户端 · 哈希标签 · 同槽多键操作
- 作业 07设计评审
同一事务需要修改一个租户多个键,其中一部分键尚未迁移为统一标签。评审过渡期命名规则及多键操作资格。
- 提交内容
- 键版本与操作矩阵
- 怎样算完成
- 不能只检查当前所在节点
- 说明旧键识别和迁移条件
练到的知识点同槽多键操作
- 作业 08编程实践
写本地客户端桩模拟 MOVED、ASK、断连和跨槽拒绝,对候选多键请求记录路由修正与执行尝试;限制事件序列长度。
- 提交内容
- 代码和状态报告
- 怎样算完成
- 不把桩响应当真实集群保证
- 每次重试保留业务身份
练到的知识点MOVED 与 ASK · 同槽多键操作
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
跨节点数据增长不均且部分客户端仍使用单机连接方式。设计扩容前检查清单和迁移后验收,给出不能通过加节点解决的问题。
- 提交内容
- 运维评审单
- 怎样算完成
- 包含各节点地址可达性
- 按槽、键热度及客户端能力分别检查
练到的知识点节点与分片 · 集群客户端 · 哈希标签 · 同槽多键操作 · 迁槽 · Sentinel 与 Cluster
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 反例构造
构造所有键此刻都在同一个节点,却仍不足以长期保证多键操作资格的部署变化,说明需要保存哪类稳定映射证据。
- 提交内容
- 反例映射与变化轨迹
- 怎样算完成
- 区分节点位置和槽关系
- 避免假设迁槽改变键的哈希规则
练到的知识点MOVED 与 ASK · 同槽多键操作
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读规范英文
Redis Cluster specification (新标签页)↗
阅读哈希槽、重定向、多键操作和失效窗口,解释集群的能力边界。
- 实践实践指南英文
Redis:Scale with Redis Cluster (新标签页)↗
阅读节点、分片和迁槽流程,规划仅在隔离环境进行的集群演练。
- 深入参考文档英文
Redis:Replication (新标签页)↗
阅读异步复制、故障切换与 WAIT 的限制,避免把副本等同于备份。
- 深入参考文档英文
CLUSTER KEYSLOT (新标签页)↗
在练习集群核对客户端计算结果。
- 深入参考文档英文
ASKING (新标签页)↗
确认临时重定向的连接级请求流程。
- 深入参考文档英文
Jedis 5.2.0 CRC16 源码 (新标签页)↗
对照 Java 客户端的字节处理和查表实现。