Redis 使用、内存与恢复边界:8.6.4 单机使用观察
前两页讲清了 Redis 的源码机制:数据结构与原子边界固定 7.2.5 源码讲类型/编码/事务,持久化与恢复讲 RDB/AOF/复制/Sentinel/Cluster。本页是 U53(#90)的 C07 使用观察页:在本机 Redis 8.6.4 standalone(DBngin,127.0.0.1:6379)上实跑 6 组使用面实验,补两页没有的真实数字——pipeline 的 RTT 摊薄、INFO memory 字段判读、SCAN 大 key 采样、CLIENT TRACKING 失效推送、Lua 脚本无回滚、SET NX 锁形态。
证据等级先说清:全部观察来自真实服务(labs/redis-usage/run.py,6 组测试、定向命名空间 u53:<run-id>:、结束零遗留,原始观察见运行证据),但都是单次观察,不是吞吐基准;AOF/复制/Sentinel/Cluster 未运行。机制问题回查前两页,本页不重讲源码。
一句话主线:使用 Redis 的成本大多不在"命令快不快",而在往返次数、内存字段误读与"看起来是锁"的错误抽象上。
pipeline:摊薄的是 RTT,不是服务端变快
同一连接串行 100 次 SET 约 3.7ms;同样 100 条命令一次 pipeline 发送约 0.3ms(U53 pipeline=PASS sequentialMs=3.9 pipelinedMs=0.5)。差距来自往返次数:串行每次都要等上一个回复,pipeline 把 100 个请求排进发送缓冲、服务端按序执行后一次性回 100 个回复。
三条边界:
- 服务端执行仍是串行的。pipeline 不减少执行成本,只减少等待;大 pipeline 一次性灌入也会在服务端排队。
- 单次观察不是基准。3.7ms 与 0.3ms 是本机 loopback 单次数字;换网络、换负载、换命令大小都会变。它证明的是"量级差异来自 RTT 摊薄",不是 P99。
- 事务语义不变。pipeline 只是打包发送;要原子性用 MULTI/EXEC 或 Lua(见下文),两者与 pipeline 可以叠加。
INFO memory:字段各答各的问题
INFO memory 的字段常被混读。本机 8.6.4 观察:used_memory 约 1.3MB、used_memory_rss 约 1.5MB、差值随 allocator 行为波动(U68 redisInfo 观察 rssMinusUsed 在两次运行间从 792KB 变到 144KB,U53 redisInfo=PASS ... allocator=libc)。三个判读要点:
used_memory是分配器视角的 Redis 自用量;used_memory_rss是操作系统视角的物理占用。RSS > used 可能是碎片或未归还页;RSS < used(很少见)意味着部分内存被换出。used_memory_peak记录历史峰值,用于观察"曾用过多少",不等于当前压力。mem_allocator=libc是本机观察;jemalloc 是常见生产配置,碎片行为不同。字段判读不等于容量结论——容量要看数据集增速与淘汰策略。
大 key:SCAN 采样,不是全量审计
KEYS 是阻塞命令,生产禁用;SCAN 游标采样是替代。本实验在本命名空间写入 200 字段 Hash 与单字段 Hash,SCAN 2 轮即扫到两者,MEMORY USAGE 分别为 14547B 与 62B(U53 bigkeyScan=PASS scannedKeys=2 bigMemoryBytes=14547 smallMemoryBytes=62 semantics=sampling-scan-not-blocking-KEYS)。
三条边界:SCAN 不保证全量(rehash 期间键可能重复或漏);MEMORY USAGE 是采样估算;"200 字段 ≈ 14KB"只是本次编码下的观察,换值大小/编码(listpack→hashtable)都会变。大 key 的治理动作(拆分、删除方式、迁移窗口)在数据结构页已讲,本页只提供"怎么找到它"。
CLIENT TRACKING:失效推送的是"键失效",不是新值
客户端缓存(CLIENT TRACKING)的推送语义常被误解为"把新值推过来"。真实观察(8.6.4,RESP3):连接 A TRACKING ON 并 GET u53:...:tracked 后,连接 B 修改该键,A 在下一条命令回复前收到独立的推送消息:
>2
$10
invalidate
*1
$20
u53:60b5ab9e:tracked(U53 clientTracking=PASS protocol=3 invalidationPushed=True semantics=invalidation-message-not-value-push。)
7.2.5 源码里这个分支写得很明确——RESP3 直接推,RESP2 既非 RESP3 也未重定向到 Pub/Sub 时什么都不发(固定源码 src/tracking.c,299–313 行):
if (c->resp > 2) {
addReplyPushLen(c,2);
addReplyBulkCBuffer(c,"invalidate",10);
} else if (using_redirection && c->flags & CLIENT_PUBSUB) {
/* We use a static object to speedup things, however we assume
* that addReplyPubsubMessage() will not take a reference. */
addReplyPubsubMessage(c,TrackingChannelName,NULL,shared.messagebulk);
} else {
/* If are here, the client is not using RESP3, nor is
* redirecting to another client. We can't send anything to
* it since RESP2 does not support push messages in the same
* connection. */
if (!(old_flags & CLIENT_PUSHING)) c->flags &= ~CLIENT_PUSHING;
return;
}这就是本实验 RESP2 下"未即时可见"的源码依据:不是延迟,是 RESP2 同连接没有推送通道。三条工程含义:
- 推送是失效通知,不是数据。客户端收到后要丢弃本地缓存并自行回源——"新值"从不经过 Redis 推送。
- RESP2 与 RESP3 行为不同。RESP2 广播模式需要客户端回复确认且推送捎带在回复前;本实验在 RESP2 下未即时观察到推送,切换
HELLO 3后推送以独立>类型到达。客户端库的协议版本是使用前提,不是可选项。 - 推送不保证及时。RESP3 也要等下一条命令触发 flush;不能假设"修改后对端立刻知道"。
Lua 脚本:原子但不回滚
脚本中途出错时,已执行的写不会回滚。真实观察:脚本依次 SET step1、INCR(对 string 报错)、SET step3,脚本以错误结束,但值停在 step1——前两步保留,第三步没执行(U53 luaAtomicity=PASS scriptError=ERR value is not an integer... valueAfterError=step1)。
这与 MULTI/EXEC 的运行期错误语义一致:原子指的是"执行期间没有别的命令插入",不是"全有或全无"。所以脚本内的写操作要自己保证幂等与可恢复;"脚本失败就重试"可能重复副作用。脚本里用 KEYS 传键(本实验如此)也是 Cluster 迁移的前提——硬编码键名会在 Cluster 下失败。
SET NX EX:锁的教学形态与误用
SET key holder NX EX 120 第二次被拒(nil),这是"互斥进入"的形态(U53 setnxLock=PASS first=OK secondRejected=True)。但两个误用要一眼识别:
- 任何客户端都能
DEL别人的锁。本实验第三步直接 DEL 成功、第三者拿到了锁——没有持有者校验,"锁"就成了礼貌性标记。正确做法是值存唯一令牌、释放用 Lua 比较-删除,或直接用带 fencing 令牌的租约。 - EX 到期不等于持有者结束。持有者卡死、锁到期、第二者进入,此时第一者恢复并 DEL,就会删掉别人的锁。
权威讲解不在本页:租约、fencing 令牌与授权裁决在[租约和 fencing 令牌](../../分布式系统与中间件/正确性与租约/租约和 fencing 令牌.md);本页只提供"SET NX 形态为什么不够"的观察。跨资源一致性协议同样只回指缓存与数据库一致性。
未运行与受阻(如实登记)
| 项目 | 状态 | 说明 |
|---|---|---|
| AOF 重写与恢复、RDB 落盘 | 未运行 | 用户实例 AOF enabled=0;不修改用户配置。源码讲解在恢复页 |
| 主从复制、PSYNC、WAIT/WAITAOF | 未运行 | 需要第二实例;恢复页已有源码与既有实验 |
| Sentinel 切换、Cluster 分槽迁移 | 未运行 | 需要多实例拓扑 |
| 吞吐/P99 基准 | 明确不纳入 | 单次观察不是基准;容量方法归容量主题 |
| 8.6.4 与 7.2.5 差异逐项核对 | 未运行 | 既有教材固定 7.2.5 源码;本页只观察 8.6.4 行为 |
实验读法
python3 labs/redis-usage/run.pypipeline:RTT 摊薄量级(单次观察)。redisInfo:INFO memory 字段判读。bigkeyScan:SCAN 采样 + MEMORY USAGE 对照。clientTracking:RESP3 失效推送原文。luaAtomicity:中途错误无回滚。setnxLock:NX 互斥 + 任意 DEL 误用。cleanup:命名空间零遗留。
闭卷验收
本页 13 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。