Chapter 07
自测与跨章辨析
前六章建立了从内存编码到 Agent 基础设施的完整链条;这一章把它们搅在一起,逼你在场景里判别该调用哪一章的方案。能在没有提示时选对,才算学会。
本章你将验证的三件事
- 取回:能不能在合上教程时把关键事实从记忆里调出来(概念层)
- 解释:能不能讲清一个行为的机制,而不只是命名它(原理层)
- 迁移:能不能在陌生场景里判别该用哪几章的方案(应用判别层——这才是目的)
用法 · 别把它当又读了一遍
所有答案集中在文末一个折叠块里。每道题先合上答案、自己在纸上或编辑器里写出来,再展开对照。直接点开答案,测试效应就没了——那等于把这一章当成第七遍阅读。三层从下往上做:概念层热身,原理层逼机制,应用判别层才是真正的检验。
7.0这套题在测什么
题目分三层,难度与「离迁移有多近」同步上升。底层只要取回事实,顶层要你在一个没见过的场景里,从六章里挑出该用的那几块拼起来。
一概念层 · 取回事实
每题一句话能答完。卡在哪一道,回那一章的对应小节补。
- 对一个有 200 个字段的 Hash 执行
OBJECT ENCODING,返回什么?(01) - String 的
embstr与raw编码以多少字节为界?(01) ziplist在哪个大版本被改名为listpack?(01)appendfsync everysec模式下宕机,最多丢多少数据?(03)maxmemory-policy的默认值是什么?这个默认意味着内存满了会发生什么?(03)- Redis Cluster 把键空间分成多少个 hash 槽?(05)
二原理层 · 解释机制
答案不是一个名词,是一条因果链。说不出「因为…所以…」就是还停在表面。
- ZSet 为什么同时维护
skiplist和dict两个结构,而不只用其一?(01) - Redis 命令执行是单线程的,为什么这反而让它快?把原因说全,不要停在「内存快」。(02)
- 单线程串行执行,是如何让每条命令「天然原子」的?这一点又支撑了后面哪些能力?(02)
UNLINK和DEL的差别是什么?Redis 4.0 为什么要加UNLINK?(02)- 近似 LRU 为什么用「随机采样 + 候选池」而不是维护一条精确的访问链表?(03)
- 副本(replica)为什么不自己删除过期的 key,而要等主库传播
DEL?(03) - Cluster 为什么用 16384 个槽,而不是 CRC16 能产生的 65536 个?(05)
- Kleppmann 对 Redlock 的核心批评是什么?他认为要保证正确性,缺了什么东西?(05)
三应用判别层 · 跨章迁移
每道题都横跨两章以上,没有标准命令可背。先判断「这属于哪块基石、牵动哪几章」,再给方案。这是全章重心。
- 省内存 + 容忍少量丢失(01 + 03):要存 8000 万条、每条很小的用户标签集合,要求内存尽量省、且允许重启丢几秒。该用什么数据类型 / 编码?持久化怎么配?为什么不上 AOF
always? - p99 突然飙高(02 + 04 + 01):监控显示某个缓存 key 是一个上百万元素的大 Hash,正被频繁
HGETALL,期间整个实例的其他请求都变慢。根因属于哪块基石?从 01 / 02 / 04 里各调用什么手段处理? - 秒杀热点(04 + 05):一个热点商品,缓存既怕击穿、又要保证扣库存绝不超卖。缓存重建该用 04 的哪种策略?扣库存的锁该用 05 的哪种方案——它在 STW 停顿下还安全吗,怎么补?
- 「写返回成功就绝不能丢」(03 + 05):能否只靠 Redis 主从 + AOF
everysec满足这个要求?如果不能,缺口分别在 03 的哪个机制和 05 的哪个机制上?结论对选型意味着什么? - 客服 Agent 选型(06 + 04 + 01):一个 Agent 要做语义缓存,外加一个十亿级知识库的向量检索。哪部分交给 Redis、哪部分该外置专用组件?各自的理由是什么?(提示:把「向量也是一种编码」和「Redis 向量的适用规模」对起来想)
亲手画一张图
合上整本教程,在纸上或 Excalidraw 里凭记忆重画那张概念地图:顶部两块基石(内存编码、单线程串行执行),往下推出各章。只画 8 个节点、标上箭头方向就够。
画完翻回起点的图 0 对照,问自己三件事:① 「命令天然原子」是从哪块基石推出来的?② 「大 key 阻塞」和「fork 持久化」分别挂在哪块基石下?③ 你画的图里,Agent 基础设施(06)的箭头是来自基石,还是被你画成了一个孤立的新模块?第三点答错,说明第六章的那句话还没真正落进去。
答案(三层都做完再展开)
概念层
- 返回
hashtable。字段数 200 超过hash-max-listpack-entries默认值 128,编码已从listpack退化为hashtable,不可逆。 - 44 字节。≤ 44 字节用
embstr(robj 与 SDS 一次分配、内存连续、对缓存友好),超过则用raw(两次分配)。该阈值在 3.2 之前是 39。 - 7.0。配置项也相应新增
-listpack-命名,旧的-ziplist-作为别名保留。 - 最多约 1 秒。
everysec由后台线程每秒fsync一次,宕机会丢失尚未刷盘的最后一秒写入;要 0 丢失得用always(但每写都刷盘、慢)。 - 默认
noeviction。内存达到maxmemory后,写命令直接报错拒绝,读和删除仍可执行——它不会替你淘汰数据,需要显式改成某种allkeys-*/volatile-*策略。 - 16384 个槽(2^14)。
原理层
- 两个结构服务两类访问:
skiplist给按分数的范围查询和排名(O(logN)),dict给「成员 → 分数」的等值查找(O(1))。只留跳表则按成员查分要扫描,只留字典则丢掉有序性,范围和排名做不了。空间换两类操作都快。 - 瓶颈本就不在 CPU,而在内存吞吐与网络。单线程省掉了锁、上下文切换、缓存行竞争这些多线程协调成本;命令本身又多是 O(1)/O(logN) 的内存操作。所以「快」来自「省掉了并发协调的开销」,而不只是「数据在内存里」。
- 单一线程把所有命令排成一条队列串行执行,任一条执行期间不会被另一条插入打断,于是命令在外部看来不可分割。
INCR、SET NX PX、Lua 脚本、分布式锁、限流令牌桶,全都建立在这条原子性之上(链向 05、06)。 DEL在主线程里同步释放对象,删一个百万元素的大 key 是 O(N)、会占住唯一线程几百毫秒。UNLINK(4.0)只在主线程 O(1) 解开引用,真正的内存释放交给后台 lazyfree 线程。4.0 这个「杀手特性」本质是把一次删除变懒,以避免大 key 阻塞整个实例。- 精确 LRU 要给每个 key 挂进一条双向链表并在每次访问时移动节点,既费内存又在热路径上加锁式开销。近似 LRU 只给每 key 存一个 24-bit 时钟,按
maxmemory-samples(默认 5)随机采样、在候选池里挑最旧的逐出;用一点精度换内存和 CPU,采样调到 10 已接近真 LRU。 - 过期判断以主库为准。若副本自行删除,主、从会因各自时钟和执行时机不同而产生数据不一致(同一时刻一边有、一边无)。副本按主库的逻辑时钟对外服务,只有等主库把
DEL/UNLINK复制过来才真正删除。 - 槽位信息要塞进每次心跳消息的位图里:16384 位 = 2KB,65536 位 = 8KB,后者让心跳包大 4 倍。且 Cluster 节点上限约 1000,16384 槽已足够细分。这是用「足够的分片粒度」换「更小的心跳开销」。
- 核心批评:GC 等 STW 停顿或时钟漂移会让一个客户端在锁早已过期后仍以为自己持有锁,于是两个客户端同时进入临界区——锁的互斥保证被打破。Kleppmann 认为要正确性必须有 fencing token(单调递增令牌,由被保护资源在写入时校验并拒绝旧令牌),而 Redlock 本身给不了这个顺序保证。antirez 则回应 Redlock 面向效率而非绝对正确,立场至今未统一。
应用判别层
- 类型/编码(01):标签若是整数 ID,用 Set,小集合走
intset/listpack编码最省;若是「在不在线」「签没签到」这类布尔位,用 Bitmap(SETBIT)按位存,8000 万位仅约 10MB。持久化(03):「允许丢几秒」正对应 RDB 或 AOFeverysec;AOFalways每写都fsync、为了一个本可放松的一致性要求牺牲大量吞吐,没必要。判断点:一致性要求的松紧直接决定持久化档位。 - 根因属于基石二(单线程,02):一个 O(N) 的
HGETALL占住唯一执行线程,所有请求排队,于是 p99 全线飙高——这是「大 key 阻塞整个实例」的典型现场。处理:01 重新设计这个大 Hash(拆分、或换更合适的结构);02 删除时用UNLINK而非DEL、用SCAN/HSCAN分批访问而非一次性HGETALL;04 把它当热点 key 治理,前面加本地缓存(Caffeine)或把它复制分散到多个 key。 - 缓存(04):热点商品用逻辑过期(物理永不过期、异步重建,期间返回旧值)彻底消除击穿的惊群;叠加随机 TTL 防同批雪崩。锁(05):扣库存是「绝不能超卖」的正确性问题。单实例
SET NX PX+ Lua 释放是起点,但在 STW 停顿 / 时钟漂移下,持锁者可能在锁过期后仍误以为持有→两个进程同时扣减。要补:要么给库存资源加 fencing token(资源端拒绝旧令牌),要么把这一步交给 ZooKeeper/etcd 这类为正确性设计的组件。判断点:efficiency 用 Redis 锁够,correctness 不能只靠它。 - 不能。03 的缺口:
everysec有最多约 1 秒的丢失窗口,「返回成功」的写可能还没落盘就宕机。05 的缺口:主从是异步复制,客户端收到 OK 时这条写未必已到副本,故障切换选了新主就丢了已确认写。两个缺口叠加 → Redis 默认配置给不了「确认即不丢」的强保证。选型含义:需要这种保证时,要么上WAIT/更强的复制确认并接受性能代价,要么把这条数据放到为持久+一致设计的存储(如带同步复制的数据库),别让 Redis 单独扛。 - 交给 Redis:语义缓存(04 缓存的语义版 + 06 的 RedisVL
SemanticCache)——它命中省 token、延迟低,规模也不大;以及会话/短期记忆。外置:十亿级向量检索该上专用向量库(Pinecone 托管十亿级、Qdrant 重过滤),因为 Redis 的原生 Vector Set / RediSearch 适合到约亿级、且VADD单线程插入会成为十亿级灌库的瓶颈。判断点:「向量也是一种编码」让 Redis 能做向量,但「单线程 + 内存成本」划定了它的规模上限——超过就外置。