读完这篇,你能做什么
能启动独立 Redis 练习实例、完成一次带过期的读写,并指出本机和容器分别使用哪个端口。
学习理由、准备要求与知识点
为什么学它
缓存与黑马点评需要可验证的 Redis 环境。先理解命令发往哪个实例,再讨论数据结构和一致性。
先读这些内容
知识点清单
- Redis 服务与 redis-cli 客户端
- Docker 镜像和容器的最小概念
- Docker 客户端与引擎状态
- 本机回环地址与端口映射
- PING 与真实应用连通性的区别
- SET、GET、TYPE 与键命名
- EX、TTL 与自动过期
- 练习实例的停止和恢复边界
运行前先确认环境沿用 Windows、Docker Desktop Linux 容器模式、Redis 7.4,本机 16379 映射容器 6379。Java 客户端源码定位 Jedis v5.2.0;算法使用 Java 17 离线解析 RESP2 Bulk String 子集,不连接 Redis,也不模拟 Redis 服务端内核。
01先弄懂它是什么
第一次使用 Redis,要先让一条命令能明确地发出去并得到结果。Redis 服务保存数据,redis-cli 发送命令;Docker 镜像是创建容器的模板,容器才是实际实例。按 Docker 官方 Windows 文档确认系统和虚拟化要求,启动 Docker Desktop,再用 docker version 同时检查客户端和服务端。只有命令能找到,还不能说明容器引擎已经可用。
确认名字没有被其他用途占用后,创建 qh-redis-study 练习容器,使用 Redis 7.4,把本机 127.0.0.1:16379 转到容器内 6379。绑定回环地址能缩小学习实例的访问范围。已有同名容器或端口冲突时,先查清用途,再选新名字或空闲端口,不要删除未知实例。下载镜像失败和 Redis 命令失败,也要分开排查。
进入容器内的 redis-cli 后,从 PING 开始,再执行 SET study:first hello EX 60、GET、TYPE 与 TTL。它们分别提供响应、值、类型与剩余有效期证据。容器内客户端连接 6379,本机 Java 应用访问 127.0.0.1:16379,两条网络路径不能互相替代。退出客户端不等于停止容器;停止自己创建的练习容器也不等于证明其中的数据已经安全备份。
文中这几个词,先认一认
- RESP
- Redis 客户端与服务端交换请求和响应的格式,需要先看响应类型,再解释里面的数据。
- 端口映射
- 把本机的一个端口转发到容器里的端口,例如本机 16379 对应容器 6379。
- Bulk String
- 用字节长度说明内容的字符串响应,也能带二进制数据;空内容不等于这个值不存在。
- TTL
- 查询时看到的键剩余有效期;没有期限和键不存在会返回不同的特殊值。
02看看它怎么工作
一个命令的实际往返包含命令编码、网络传输、服务端执行和响应解码。TCP 传递的是字节流,并不保证一次 read 就取得完整响应。比如 hello 的 Bulk String 响应包含长度头、五个载荷字节和结尾分隔符;长度头和载荷可能分几次到达,也可能与下一条响应连在一次读取中。
因此客户端必须维护协议边界。先读类型标记,再按该类型读取整数、文本行或固定长度载荷;只有完整帧到齐才能交给调用者。长度指字节数,不是 Java String 的字符数量。中文经过 UTF-8 编码后通常占多个字节,若按字符数截取,会让当前响应与后续响应错位。
PING 返回 PONG 只验证这次命令往返。它没有验证登录权限是否足够执行其他命令、应用使用的映射端口是否正确、过期策略是否符合业务要求,也没有测试持久化和恢复。学习时先保持单实例、小数据和明确前缀,再逐步增加数据结构与故障条件,避免一开始就把多个层面的未知混在一起。
先跑通一个最小例子
先确认 Docker Desktop 已启动并处于 Linux 容器模式。本例创建一个名字独立的本机 Redis 练习容器,主机仅开放 127.0.0.1:16379;命令行客户端通过 docker exec 在容器内访问 6379。名字或端口已占用时先检查用途,再选择新的练习名称。
docker version
docker run --name qh-redis-lab -d -p 127.0.0.1:16379:6379 redis:7.4
docker exec qh-redis-lab redis-cli PING
docker exec qh-redis-lab redis-cli SET qh:lab:topic Java EX 60
docker exec qh-redis-lab redis-cli GET qh:lab:topic
docker exec qh-redis-lab redis-cli TTL qh:lab:topic
docker exec qh-redis-lab redis-cli TYPE qh:lab:topic核对运行结果PING 返回 PONG,SET 返回 OK,立即 GET 返回 Java,TYPE 为 string。TTL 随时间递减,可能为 0 到 60,过期后为 -2;不能把倒计时断言成每次恰好 60。这个练习未配置可靠持久化方案,也不等于生产 Redis 部署验收。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
A["本机客户端访问 16379"] --> B["Docker 映射到容器 6379"]
B --> C["Redis 解析并执行命令"]
C --> D["RESP 字节流返回"]
D --> E["客户端读取类型和长度"]
E --> F{"完整响应是否到齐"}
F -->|"否"| G["保留缓冲区并继续读取"]
G --> E
F -->|"是"| H["交付结果并保留剩余字节"]按顺序看,每一步做了什么
- 确认容器引擎与隔离实例存在,记录内外端口。
- 分别验证容器内 PING 与应用实际访问路径。
- 读取带长度的响应时累计字节,不依赖单次 read 完成。
- 核对值、类型和 TTL,再退出并说明哪些运行状态仍然存在。
03对照实现代码
Jedis v5.2.0 · redis.clients.jedis.Protocol#process、processBulkReply、processMultiBulkReply
所链接真实 Java 文件与方法已核验。以下为原创局部模型,只表现类型分派和固定长度读取,省略 Jedis 的 RESP3、异常分类、缓冲区及连接管理。Redis 服务端不是用这段 Java 实现;这里分析的是 Java 客户端。
class SourceModel {
interface ReplyInput {
int nextByte();
String line();
long integerLine();
byte[] exactBytes(int length); // 实现必须循环读取直到足够或报断流
void requireCrLf();
}
static Object read(ReplyInput input) {
int type = input.nextByte(); // 先消费响应类型
if (type == '+') return input.line();
if (type == ':') return input.integerLine();
if (type != '$') throw new IllegalArgumentException("此模型不支持该类型");
long length = input.integerLine();
if (length == -1) return null; // 缺失值不等于空数组
if (length < 0 || length > 1024) throw new IllegalArgumentException("长度越界");
byte[] bytes = input.exactBytes((int) length); // 长度按字节计算
input.requireCrLf(); // 教学模型显式验证终止分隔符
return bytes;
}
}Protocol.process 先读一个表示响应类型的字节,再分别处理错误、整数或字节串。不能先把整条回复当普通字符串,再猜它是什么。看起来同样是数字的文本,在整数回复和 Bulk String 中,也不一定应转成同一种 Java 类型。
真实 processBulkReply 先读取长度,长度为 -1 时返回 null,否则分配对应数组并循环读取剩余字节。循环说明一次 read 不保证完成固定长度载荷。教学模型增加了很小的长度上限并显式校验 CRLF,这是为了展示受限解析器的防御边界,不宣称与真实实现逐行等价。
数组回复会逐项继续处理响应,Jedis 在相应路径保留部分数据异常为数组项。这个细节对以后理解 EXEC 很重要:一次往返可以包含多个结果,其中某项失败不表示其他项都没有执行。初学时先掌握类型和长度,之后再阅读嵌套数组和事务错误。
连接层负责发送命令、读取完整响应并构建 API 返回值,协议层负责解释字节。发生连接中断时,客户端有可能不知道命令是否已在服务端执行;不能根据读不到响应就自动判定写入没发生。入门 SET 看似简单,但这个不确定窗口正是后续幂等与重试课程的起点。
代码先调用谁,再调用谁
Connection.executeCommand组织并发送参数。Protocol.sendCommand编码数组与参数字节长度。Protocol.read进入 process 类型分派。- 读取完整载荷后由客户端 API 构建具体结果。
04动手写一个例子
解析一个可能尚未到齐的 RESP2 Bulk String
输入接收缓冲区与最大允许载荷,解析从索引零开始的一帧 Bulk String。完整帧返回载荷和消费字节数,数据不足返回 complete=false,格式非法或超长则拒绝。null 表示协议中的缺失值,长度零返回空数组;不解析 RESP3、嵌套数组或其他回复类型。
- 输入 1
$5\r\nhello\r\n- 输出 1
complete=true, hello, consumed=11
- 输入 2
$0\r\n\r\n- 输出 2
complete=true, 空数组
- 输入 3
$-1\r\n- 输出 3
complete=true, null
- 输入 4
$5\r\nhe- 输出 4
complete=false
先寻找长度头的 CRLF。如果还没到齐,不能把缺失数据判断成错误;但首字节已经不是美元符时可以立即拒绝。完整长度头必须满足允许的十进制语法,再检查 -1 和容量上限,避免按不可信长度分配任意大小数组。
知道头长度后,整帧需要头、载荷和末尾两个字节。计算使用 long,防止长度相加溢出。缓冲区不足时保持消费量为零,让调用方保留当前内容;完整时只消费一帧,后面的字节属于其他响应,不应一并丢弃。
本题复制载荷以获得独立结果。main 固定验证普通文本、空值、缺失值、半包、中文、非法分隔符与超长输入。只调用纯解析方法,不建立网络连接;编译 Main.java 后运行 Main,任何与期望不符都会抛 AssertionError。
为什么这样写是对的complete=false 时没有消费任何字节;complete=true 时 consumed 恰好越过当前帧的 CRLF,payload 长度与头声明一致。
一次调用扫描头并复制长度为 m 的载荷,最多线性于本次输入。真实增量解析应保存扫描位置,避免每收到一个字节都从头扫描导致累计平方成本。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class Main {
record Result(boolean complete, byte[] payload, int consumed) {}
static Result parse(byte[] data, int limit) {
if (data == null || limit < 0) throw new IllegalArgumentException();
if (data.length == 0) return new Result(false, null, 0);
if (data[0] != '$') throw new IllegalArgumentException();
int end = -1;
for (int i=1; i+1<data.length; i++) {
if (data[i]=='\r' && data[i+1]=='\n') { end=i; break; }
}
if (end < 0) return new Result(false, null, 0);
String text = new String(data, 1, end-1, StandardCharsets.US_ASCII);
if (!text.matches("-1|[0-9]+")) throw new IllegalArgumentException();
long length;
try { length=Long.parseLong(text); }
catch (NumberFormatException e) { throw new IllegalArgumentException(e); }
int start=end+2; // 头部结束后才开始计算载荷边界
if (length == -1) return new Result(true, null, start);
if (length > limit) throw new IllegalArgumentException();
long needed=(long)start+length+2; // 相加不能溢出 int
if (needed > data.length) return new Result(false, null, 0);
int tail=start+(int)length;
if (data[tail]!='\r' || data[tail+1]!='\n') throw new IllegalArgumentException();
return new Result(true, Arrays.copyOfRange(data,start,tail), tail+2);
}
static byte[] bytes(String s) { return s.getBytes(StandardCharsets.UTF_8); }
static void check(boolean condition) { if (!condition) throw new AssertionError(); }
static void reject(String wire, int limit) {
try { parse(bytes(wire),limit); throw new AssertionError("应拒绝"); }
catch (IllegalArgumentException expected) { /* 期望的格式错误 */ }
}
public static void main(String[] args) {
// 固定期望覆盖正常、空值和错误边界,失败立即中断验证
Result one=parse(bytes("$5\r\nhello\r\nextra"),100);
check(one.complete && one.consumed==11 && Arrays.equals(one.payload,bytes("hello")));
Result empty=parse(bytes("$0\r\n\r\n"),100); check(empty.complete && empty.payload.length==0);
Result absent=parse(bytes("$-1\r\n"),100); check(absent.complete && absent.payload==null);
check(!parse(bytes("$5\r\nhe"),100).complete);
check(!parse(new byte[0],100).complete);
check(Arrays.equals(parse(bytes("$6\r\n中文\r\n"),100).payload,bytes("中文")));
reject("$3\r\nabcXX",100);
reject("$1000\r\n",10);
reject("$-2\r\n",100);
System.out.println("PASS 9 RESP tests");
}
}这些特殊情况也要试一试
- 半包不是协议错误,应等待更多数据;但必须另设最大缓冲与超时。
- 空数组与
null分别表示空值和不存在,调用者不能混为一谈。 - 载荷可能包含 CRLF,读取必须按长度而非搜索第一个换行。
进阶选读 · 生产拓展:真正的增量状态机与背压
场景代理或客户端持续接收大响应与多条连在一起的响应,不能每次复制并从头扫描。
做法维护读索引、类型、长度和剩余载荷状态,限制头长度与总缓冲,按需交付完整帧;下游处理慢时暂停读取或限制在途请求。
复杂度每个字节只进入有限状态一次可达到累计 O(n);缓冲空间由最大帧和背压策略约束。
好处与代价零拷贝切片减少复制,但需要清晰的缓冲区所有权;复制更简单,却增加分配和内存带宽成本。
05项目里怎么用
入门容器只作为隔离实验,保持 study: 前缀,不使用 FLUSHALL 清理未知环境。没有设计数据卷、AOF、备份和恢复验证时,不让它承载重要业务数据。容器仍在与客户端仍连接是两个独立状态,重建容器、重启进程和键自然过期也不能视为同一种删除原因。
应用连不上时先记录访问位置,再核对映射。容器内 exec 得到 PONG,验证的是内部入口;本机应用走映射端口,还涉及主机地址与转发。遇到认证、权限或连接错误应分别定位,不通过开放公网端口或关闭所有访问控制来替代诊断。
协议解析在生产环境需要长度、缓冲和时间预算,不能相信远端总会按预期发送小响应。大键读取即使 Redis 执行快,也可能在网络和 Java 堆上产生压力。观察命令延迟、返回字节数、超时与连接复用状态,再决定拆分数据、分页或限制请求规模。
容易踩的坑
- 按 Java 字符数切割 UTF-8 载荷,会破坏协议边界。
- 一次 socket read 返回少量字节,不表示响应结束。
- 本机映射端口与容器端口混用,常使两种客户端得到不同结果。
- PING 成功不证明写权限、持久化、负载和恢复都已验证。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 容器内 redis-cli | 排除宿主机映射因素,快速验证进程与命令。 | 不能代表应用实际网络路径。 | 确认 Redis 本身是否能响应。 |
| 应用同路径连接测试 | 覆盖真实端点、凭据与驱动。 | 需要维护与应用一致的配置和受控测试键。 | 排查命令行可用但应用失败。 |
| 独立持久化练习实例 | 可安全验证重启与恢复。 | 需要额外管理卷、备份和失败注入。 | 已掌握基本命令后研究数据可靠性。 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01为什么退出 redis-cli 不会自动删除键?
展开参考答案与推导
客户端会话不拥有全部服务端数据。键由 Redis 实例维护,可能因显式删除、过期、淘汰或实例数据丢失而消失;断开连接只是结束本次会话。
02Bulk String 的长度为什么用字节?
展开参考答案与推导
网络协议传输字节,且载荷可能不是文本。字符编码会让字符数与字节数不同,按字节才能稳定界定当前帧。
03RESP 的 null 与空串如何区分?
展开参考答案与推导
Bulk String 长度 -1 表示不存在值,长度 0 表示存在的零字节载荷。应用应保留这种区别,而不是统一转换为空字符串。
换个情况,还会处理吗
01容器内 PONG 正常,本机应用超时,先检查什么?
展开参考答案与推导
核对宿主机绑定地址、16379 映射、应用使用的主机端口和实际运行位置,再看认证及驱动配置;不要把内部验证结果当作外部路径证明。
02GET 大值后 Java 内存猛增,如何判断瓶颈?
展开参考答案与推导
同时查看返回字节量、并发在途请求、客户端缓冲和对象分配;服务端命令快不排除网络与堆分配压力,应限制响应规模再比较。
03收到半个响应后断连,能否立即重试写命令?
展开参考答案与推导
不能仅凭缺少完整响应判断写未执行。先根据命令语义评估重试是否幂等,必要时使用稳定业务标识或查询结果恢复。
写代码试一试
01解析粘在一起的两个 Bulk String。
- 输入 / 条件
$1\r\na\r\n$1\r\nb\r\n- 输出 / 预期
第一帧 a,消费 7 字节;剩余帧 b
提示 使用 consumed 推进读索引。
展开参考答案与推导
交付第一帧后只移除已消费前缀,再解析剩余字节。不得因首次解析成功就清空整个缓冲区。
02如何拒绝无限长且没有 CRLF 的长度头?
- 输入 / 条件
美元符后持续发送数字,不发送换行- 输出 / 预期
超过头部预算即拒绝
提示 不完整输入也应有上限。
展开参考答案与推导
为长度头设置最大字节数和读取期限,在寻找分隔符时累计检查;不能无限返回 incomplete 并任由缓冲增长。
03载荷中包含换行怎样解析?
- 输入 / 条件
$4\r\na\r\nb\r\n- 输出 / 预期
载荷为四字节 a、CR、LF、b
提示 只使用声明长度确定载荷尾。
展开参考答案与推导
先读四个载荷字节,再验证额外的末尾 CRLF。若扫描载荷中的首个换行就结束,会把合法二进制内容错误截断。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能启动独立 Redis 练习实例、完成一次带过期的读写,并指出本机和容器分别使用哪个端口。
本篇重点加练
初次连通必须核对实际实例、键与协议行为,PING 成功只能作为最初一步证据。
- PING 与真实应用连通性的区别
- SET、GET、TYPE 与键命名
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
准备第一次 Redis 实验,说明 redis-cli、Redis 服务、Docker 镜像和运行容器分别是什么,并画出一条 GET 请求经过的组件。
- 提交内容
- 组件图与术语表
- 怎样算完成
- 区分安装工具与启动服务
- 标出命令在哪个环境执行
练到的知识点Redis 服务与 redis-cli 客户端 · Docker 镜像和容器的最小概念
- 作业 02排错实验
在本机检查 Docker 客户端与引擎状态,启动仅回环可访问的独立 Redis 实例;记录容器端口与宿主端口映射。
- 提交内容
- 环境核查与启动记录
- 怎样算完成
- 不占用现有实例端口
- 引擎不可用时明确停止在哪一步
练到的知识点Docker 客户端与引擎状态 · 本机回环地址与端口映射
- 作业 03代码推演
容器内 PING 成功后,本地 Java 程序要读一个中文值。列出从网络到键名、类型和内容的逐项验证,说明 PING 未覆盖哪些部分。
- 提交内容
- 验证清单与读写记录
- 怎样算完成
- 键有独立实验前缀
- 记录连接地址及数据编码
练到的知识点PING 与真实应用连通性的区别 · SET、GET、TYPE 与键命名
- 作业 04排错实验
写入带 EX 的实验键,分别在命令行退出、容器停止和重新启动后观察状态。先说明本次实例是否配置持久化,再记录 TTL 与值。
- 提交内容
- 时刻表与生命周期记录
- 怎样算完成
- 不删除其他容器或卷
- 区分自然过期与实例数据恢复
练到的知识点EX、TTL 与自动过期 · 练习实例的停止和恢复边界
- 作业 05原理问答
机器上有两个 Redis 实例,两个都能返回 PONG。设计能确认两个 redis-cli 终端访问同一实验实例的证据,避免误改其他数据。
- 提交内容
- 实例身份与核验清单
- 怎样算完成
- 记录完整连接目标
- 使用独立实验标记而非猜测端口
练到的知识点PING 与真实应用连通性的区别
- 作业 06代码推演
键名包含中文、空格及看似相同的 Unicode 字符,命令行与程序各写一次。分析显示相同和字节相同之间需要验证的部分。
- 提交内容
- 键与编码样本
- 怎样算完成
- 明确字符到字节转换
- 区分不存在、空值和错误类型
练到的知识点SET、GET、TYPE 与键命名
综合应用
- 作业 07编程实践
写本地 Java 小程序完成连接、写入、读取、类型检查和短期过期观察,覆盖不存在键及类型不符,使用有界连接与读取超时。
- 提交内容
- 源码、运行命令及日志
- 怎样算完成
- 无真实口令硬编码
- 结束后能清理自己的键和连接
练到的知识点Redis 服务与 redis-cli 客户端 · 本机回环地址与端口映射 · PING 与真实应用连通性的区别 · SET、GET、TYPE 与键命名 · EX、TTL 与自动过期 · 练习实例的停止和恢复边界
- 作业 08排错实验
在两个 redis-cli 终端显式记录连接的本地实验目标,用独立前缀和本次唯一标记执行 SET、GET、TYPE,再交换写入与读取端核对。可提交命令脚本或逐步记录,无需编写 Java 客户端。
- 提交内容
- 命令与交叉核验记录
- 怎样算完成
- 清理仅限自己创建的键
- 不只输出 PONG 宣布验收成功
练到的知识点PING 与真实应用连通性的区别 · SET、GET、TYPE 与键命名
进阶挑战 · 掌握前置后选做
- 作业 09设计评审
同学复制你的步骤到另一台电脑后失败。编写一份按环境、引擎、端口、服务和应用逐层定位的实验说明,附停止与恢复边界。
- 提交内容
- 可复现实验文档
- 怎样算完成
- 每层有明确验证动作
- 不得用删除全部容器作为恢复步骤
练到的知识点Docker 镜像和容器的最小概念 · Docker 客户端与引擎状态 · 本机回环地址与端口映射 · PING 与真实应用连通性的区别 · SET、GET、TYPE 与键命名 · 练习实例的停止和恢复边界
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 跨域迁移
将这套连通性验收迁移到数据库或模型 API,提出“端点活着”与“当前应用能正确完成目标操作”之间应补充的证据。
- 提交内容
- 跨域验收对照
- 怎样算完成
- 选择具体操作而非空泛健康检查
- 不要求访问任何真实外部账号
练到的知识点PING 与真实应用连通性的区别 · SET、GET、TYPE 与键命名
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 实践实践指南英文
Docker Desktop:Windows 安装 (新标签页)↗
检查 Windows、WSL 2 和虚拟化前提,完成安装后先确认引擎启动。已安装者直接验证 docker version。
- 必读教程英文
Redis:使用 Docker 运行 (新标签页)↗
阅读镜像启动、端口映射与容器内 redis-cli 的方式;本篇固定用 Redis 7.4 做隔离练习。
- 必读参考文档英文
Redis CLI 使用手册 (新标签页)↗
学习交互模式、主机与端口参数;区分当前命令运行在主机还是容器中。
- 深入参考文档英文
Redis:PING 命令 (新标签页)↗
核对 PONG 表示的连通性边界,理解它不能代替数据读写、权限或持久化验证。
- 深入参考文档英文
Redis protocol specification (新标签页)↗
阅读 RESP2 类型、Bulk
String长度及空值表达。 - 深入参考文档英文
Jedis v5.2.0 Protocol.java (新标签页)↗
核对类型分派、固定长度循环读取与数组回复处理。