qinghuan
← 数据系统 / 全部笔记

单元 11 / 数据系统

P0 · 必会主线L0 · 零基础

从零准备 Redis:启动隔离实例并读写第一个键

启动独立 Redis 7.4 练习容器,分清本机与容器端口,再用 PING、SET、GET 和 TTL 核对结果。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能启动独立 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。名字或端口已占用时先检查用途,再选择新的练习名称。

PowerShell:启动并检查专用练习容器 · PowerShell
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["交付结果并保留剩余字节"]

按顺序看,每一步做了什么

  1. 确认容器引擎与隔离实例存在,记录内外端口。
  2. 分别验证容器内 PING 与应用实际访问路径。
  3. 读取带长度的响应时累计字节,不依赖单次 read 完成。
  4. 核对值、类型和 TTL,再退出并说明哪些运行状态仍然存在。

03对照实现代码

Jedis 如何根据 RESP 标记分派响应解码 (新标签页)↗

Jedis v5.2.0 · redis.clients.jedis.Protocol#process、processBulkReply、processMultiBulkReply

所链接真实 Java 文件与方法已核验。以下为原创局部模型,只表现类型分派和固定长度读取,省略 Jedis 的 RESP3、异常分类、缓冲区及连接管理。Redis 服务端不是用这段 Java 实现;这里分析的是 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 看似简单,但这个不确定窗口正是后续幂等与重试课程的起点。

代码先调用谁,再调用谁

  1. Connection.executeCommand 组织并发送参数。
  2. Protocol.sendCommand 编码数组与参数字节长度。
  3. Protocol.read 进入 process 类型分派。
  4. 读取完整载荷后由客户端 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 长度与头声明一致。

时间 O(n)额外空间 O(m)

一次调用扫描头并复制长度为 m 的载荷,最多线性于本次输入。真实增量解析应保存扫描位置,避免每收到一个字节都从头扫描导致累计平方成本。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 48 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
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 与键命名

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    准备第一次 Redis 实验,说明 redis-cli、Redis 服务、Docker 镜像和运行容器分别是什么,并画出一条 GET 请求经过的组件。

    提交内容
    组件图与术语表
    怎样算完成
    • 区分安装工具与启动服务
    • 标出命令在哪个环境执行

    练到的知识点Redis 服务与 redis-cli 客户端 · Docker 镜像和容器的最小概念

  2. 作业 02排错实验

    在本机检查 Docker 客户端与引擎状态,启动仅回环可访问的独立 Redis 实例;记录容器端口与宿主端口映射。

    提交内容
    环境核查与启动记录
    怎样算完成
    • 不占用现有实例端口
    • 引擎不可用时明确停止在哪一步

    练到的知识点Docker 客户端与引擎状态 · 本机回环地址与端口映射

  3. 作业 03代码推演

    容器内 PING 成功后,本地 Java 程序要读一个中文值。列出从网络到键名、类型和内容的逐项验证,说明 PING 未覆盖哪些部分。

    提交内容
    验证清单与读写记录
    怎样算完成
    • 键有独立实验前缀
    • 记录连接地址及数据编码

    练到的知识点PING 与真实应用连通性的区别 · SET、GET、TYPE 与键命名

  4. 作业 04排错实验

    写入带 EX 的实验键,分别在命令行退出、容器停止和重新启动后观察状态。先说明本次实例是否配置持久化,再记录 TTL 与值。

    提交内容
    时刻表与生命周期记录
    怎样算完成
    • 不删除其他容器或卷
    • 区分自然过期与实例数据恢复

    练到的知识点EX、TTL 与自动过期 · 练习实例的停止和恢复边界

  5. 作业 05原理问答

    机器上有两个 Redis 实例,两个都能返回 PONG。设计能确认两个 redis-cli 终端访问同一实验实例的证据,避免误改其他数据。

    提交内容
    实例身份与核验清单
    怎样算完成
    • 记录完整连接目标
    • 使用独立实验标记而非猜测端口

    练到的知识点PING 与真实应用连通性的区别

  6. 作业 06代码推演

    键名包含中文、空格及看似相同的 Unicode 字符,命令行与程序各写一次。分析显示相同和字节相同之间需要验证的部分。

    提交内容
    键与编码样本
    怎样算完成
    • 明确字符到字节转换
    • 区分不存在、空值和错误类型

    练到的知识点SET、GET、TYPE 与键命名

综合应用

  1. 作业 07编程实践

    写本地 Java 小程序完成连接、写入、读取、类型检查和短期过期观察,覆盖不存在键及类型不符,使用有界连接与读取超时。

    提交内容
    源码、运行命令及日志
    怎样算完成
    • 无真实口令硬编码
    • 结束后能清理自己的键和连接

    练到的知识点Redis 服务与 redis-cli 客户端 · 本机回环地址与端口映射 · PING 与真实应用连通性的区别 · SET、GET、TYPE 与键命名 · EX、TTL 与自动过期 · 练习实例的停止和恢复边界

  2. 作业 08排错实验

    在两个 redis-cli 终端显式记录连接的本地实验目标,用独立前缀和本次唯一标记执行 SET、GET、TYPE,再交换写入与读取端核对。可提交命令脚本或逐步记录,无需编写 Java 客户端。

    提交内容
    命令与交叉核验记录
    怎样算完成
    • 清理仅限自己创建的键
    • 不只输出 PONG 宣布验收成功

    练到的知识点PING 与真实应用连通性的区别 · SET、GET、TYPE 与键命名

进阶挑战 · 掌握前置后选做

  1. 作业 09设计评审

    同学复制你的步骤到另一台电脑后失败。编写一份按环境、引擎、端口、服务和应用逐层定位的实验说明,附停止与恢复边界。

    提交内容
    可复现实验文档
    怎样算完成
    • 每层有明确验证动作
    • 不得用删除全部容器作为恢复步骤

    练到的知识点Docker 镜像和容器的最小概念 · Docker 客户端与引擎状态 · 本机回环地址与端口映射 · PING 与真实应用连通性的区别 · SET、GET、TYPE 与键命名 · 练习实例的停止和恢复边界

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 跨域迁移

    将这套连通性验收迁移到数据库或模型 API,提出“端点活着”与“当前应用能正确完成目标操作”之间应补充的证据。

    提交内容
    跨域验收对照
    怎样算完成
    • 选择具体操作而非空泛健康检查
    • 不要求访问任何真实外部账号

    练到的知识点PING 与真实应用连通性的区别 · SET、GET、TYPE 与键命名

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗