R Redis 学习手册
下载 EPUB
一本可以动手验证的 Redis 学习书

Redis 学习手册

从数据结构到生产实践

以 Redis 7.2 为实验基线,循序掌握数据建模、可靠性与工程边界。

20 章 + 前言与附录可运行示例EPUB 离线阅读

前言:学会做判断

Redis 的命令容易上手,困难的是回答工程问题:这个键为什么占这么多内存?主节点确认写入以后,故障切换还能丢数据吗?给缓存设置过期时间,是否就能保证数据一致?本书围绕这些问题展开,从可以复现的命令走向可以解释的系统行为。

本书适合会使用终端、有基本编程与数据库知识的读者。你不需要先读 Redis 源码,但应知道进程、网络连接和磁盘文件的含义。每章按“问题—机制—实验—边界—练习”组织。先预测实验结果,再运行命令,比逐行照抄更有效。

版本与实验约定

实验以 Redis 7.2 为兼容基线,使用官方容器镜像的 7.2 系列。它是教学基线,并不代表对生产版本的推荐。补丁版本会更新;需要严格复现时记录镜像摘要和 INFO server 的输出。Redis 8 及后续版本增加的能力、默认配置变化、许可及分发方式,应以目标版本官方文档为准。本书不把某个托管产品的保证当成所有 Redis 部署的保证。

命令示例中的 127.0.0.1:6379> 提示符被省略,代码块里的 Redis 命令可在交互式 redis-cli 中逐行执行。sh 代码块在系统终端执行。涉及多客户端的实验会标明客户端 A、B。返回结果通常用正文描述,避免把输出误作下一条命令。键统一使用 lab: 前缀;综合项目用 book: 前缀。

所有会改变持久化、内存或故障状态的实验,都应使用独立实例。不要把示例指向生产地址。示例口令只用于理解配置,部署时应通过自己的密钥管理系统提供。实验不要求清空整个数据库。

阅读路径

第一次接触 Redis,依次阅读第 1—6 章;准备设计业务,继续阅读第 7—14 章;承担上线运维,重点完成第 15—18 章。第 19 章把数据建模、原子性、幂等与过期策略合在一个库存预留项目中,第 20 章提供练习参考答案和复盘方法。

本书所有文字和实验均为本项目原创编排,技术依据集中列在附录。Redis 是相关权利人的商标,本书不是 Redis 官方出版物。示意时序省略了网络协议细节;遇到部署决策,应进一步阅读相应版本的官方说明并在自己的环境中验证。

第 1 章 认识 Redis 与建立实验环境

1.1 从一次请求看 Redis

Redis 是一个通过网络访问的数据结构服务器。客户端发送命令,服务器读取键对应的值、执行操作并返回结果。它把活跃数据放在内存中,因此常见操作延迟很低;持久化、复制和淘汰策略决定了内存以外的可靠性边界。

“内存数据库”不意味着数据必然不落盘,“单线程”也不意味着进程只有一条线程。许多常规命令的执行由主线程串行处理,但网络 I/O、后台释放、持久化子进程等会参与工作。判断吞吐瓶颈时,要区分命令执行、网络、客户端排队和磁盘等待。

Redis 擅长缓存、计数、排行、短期状态和快速索引。它没有关系数据库那样通用的跨表事务与约束。将核心账本迁入 Redis 之前,必须明确可接受的数据损失、审计方式以及故障恢复流程。

1.2 启动隔离实例

仓库提供 compose.yaml,需要 Docker 及 Compose 插件。启动命令:

docker compose up -d
docker compose exec redis redis-cli PING
docker compose exec redis redis-cli INFO server

PING 应返回 PONG。端口只绑定本机回环地址,数据保存在独立命名卷。进入交互终端:

docker compose exec redis redis-cli

随后执行:

SET lab:hello "Redis"
GET lab:hello
TYPE lab:hello
DEL lab:hello

依次应看到写入成功、字符串 Redis、类型 string 和删除数量 1。停止容器但保留数据用 docker compose down;只有确认不再需要实验数据时才附加 --volumes。

1.3 先认识三种错误

连接失败通常是网络、监听地址或进程问题;NOAUTH 是认证问题;WRONGTYPE 表示键存在但类型不匹配。三者发生在不同层次,不能靠同一条“重启服务”解决。

SET lab:type "text"
LPUSH lab:type item

第二条命令应报 WRONGTYPE。删除并重新建模才可能转换数据类型;不能用列表命令直接操作字符串。客户端应把这类错误当作数据模型或代码问题,而非瞬时网络错误自动重试。

1.4 延迟预算

一次调用耗时包含连接获取、网络往返、服务端排队、命令执行和响应传输。假设单次往返为 1 毫秒,顺序执行 100 次命令,即使命令本身很快,也可能消耗接近 100 毫秒。先减少往返,往往比猜测底层编码更有效。

练习:写入一个用户昵称,再用错误类型命令读取它。分别记录连接错误、键不存在和类型错误在客户端中的表现。思考:将错误都转换成“没有数据”,会隐藏哪些故障?

第 2 章 键、命名空间与生命周期

2.1 键设计就是接口设计

键名应表达业务实体、标识和用途,例如 book:user:42:profile。环境通常由独立实例或集群隔离;租户信息是否进入键名取决于隔离策略。不要把邮箱、手机号或访问令牌直接拼进键名,因为监控、慢日志和备份都有机会暴露它们。

键越长,重复存储的开销越大;键过短又难以运维。把命名规则写成表格,比让每个调用方自由发挥更可靠。

形状 类型 生命周期 用途
book:user:42:profile Hash 随业务失效 用户摘要
book:rank:2026-10 Sorted Set 月末归档后过期 月排行
book:request:abc String 按重试窗口保留 幂等结果

2.2 过期是状态的一部分

SET lab:session token EX 60
TTL lab:session
PTTL lab:session
EXPIRE lab:session 120
PERSIST lab:session
TTL lab:session

TTL 以秒返回剩余有效期,PTTL 使用毫秒;-1 表示存在且没有过期时间,-2 表示键不存在。普通 SET 覆盖键时会移除原 TTL,除非使用 KEEPTTL;INCR、HSET 等原位修改通常保留键的 TTL。

SET lab:ttl 1 EX 60
INCR lab:ttl
TTL lab:ttl
SET lab:ttl 3
TTL lab:ttl

最后返回 -1。这类覆盖操作经常造成“缓存为何一直不更新”的问题。需要初次创建和过期一体完成时用 SET ... EX,不要在应用中分成 SET 和 EXPIRE 两个独立步骤。

2.3 遍历与删除

KEYS lab:* 会一次扫描整个键空间,数据量大时阻塞服务。SCAN 用游标渐进遍历,返回数量受 COUNT 提示影响,不保证每次恰好多少条,也可能重复返回键。一次完整迭代对始终存在的键有相应遍历保证,但不是一致性快照;并发增删的键是否出现没有完整保证。

SCAN 0 MATCH lab:* COUNT 100

把返回的新游标继续传入,直到游标回到 0;不能因为本批为空就停止。批量处理要幂等,并限制每次删除数量。大对象使用 UNLINK 可把主要内存回收工作放到后台,但仍要评估对象结构和异步释放积压。

2.4 数据库编号的限制

独立 Redis 可以使用逻辑数据库编号,但它们共享进程资源,并不形成可靠的租户隔离。Redis Cluster 只支持数据库 0。因此应用尽量显式命名和配置,不要依赖“把数据库编号加一”完成水平扩展。

练习:用 SET、INCR 和 SET KEEPTTL 比较 TTL。设计一个批量清理脚本时,为什么同时需要游标、批次上限和可重复执行能力?

第 3 章 String:缓存、计数与条件写入

3.1 值是字节序列

String 可以保存 UTF-8 文本、JSON、整数的文本形式或二进制内容。Redis 不会替你检查 JSON 中字段是否符合业务约束。序列化格式需要约定版本、空值表现和兼容方式;应用升级后旧缓存仍可能存在。

SET lab:article:1 '{"v":1,"title":"认识 Redis"}' EX 300
GET lab:article:1
MSET lab:a 10 lab:b 20
MGET lab:a lab:b lab:missing

MGET 按请求顺序返回值,不存在的位置是空值。一次多键调用减少网络往返;在 Cluster 中,多键命令还必须满足同槽约束。

3.2 计数的原子边界

SET lab:views 0
INCR lab:views
INCRBY lab:views 9
GET lab:views

最终是 10。INCR 避免了客户端“读—加一—写”之间的竞争,它使用有符号 64 位整数,溢出会报错。浮点计数不适合精确货币;金额可用最小货币单位的整数表示,同时检查范围。

如果写成 INCR 后再 EXPIRE,两步之间进程可能失败,产生永不过期的计数键。用脚本把递增和首次过期放在一起,见第 7 章。

3.3 条件写入

SET lab:once first NX EX 30
SET lab:once second NX EX 30
GET lab:once

若初始键不存在,第一次成功,第二次返回空值,读到 first。NX 只在不存在时写,XX 只在存在时写。返回空值是条件未满足,并不等同于网络超时。

条件写入可以实现短期去重的占位,但“占位成功”并不代表后续业务已完成。业务失败后如何解除占位、重试是否会重复扣款、超时后结果能否查询,都需要额外协议。把去重当成完整的幂等实现会留下漏洞。

3.4 Bitmap 是位操作视角

SETBIT lab:attendance 7 1
GETBIT lab:attendance 7
BITCOUNT lab:attendance

偏移量以位计数。用户 ID 极大且稀疏时,直接作为位偏移会分配大量零填充空间;它适合稠密编号。按天保存签到位图,可通过位运算求连续活跃,但应控制键大小和运算成本。

练习:为什么“先 GET,不存在才 SET”无法保证只有一个客户端初始化?若某 API 超时,你能否根据超时推断 INCR 没有执行?给出安全的重试设计思路。

第 4 章 Hash、List 与 Set 的业务模型

4.1 Hash:把同一实体的字段放在一起

HSET lab:user:42 name Lin level 3
HGET lab:user:42 name
HMGET lab:user:42 name level missing
HINCRBY lab:user:42 level 1
HGETALL lab:user:42

Hash 适合用户摘要或配置字段,避免每改一个字段都序列化完整 JSON。在本书的 7.2 基线中,TTL 设置在整个键上,不能把新版本的字段级过期能力假设成基线能力。若字段需要独立过期,可拆键或由应用维护时间戳与清理流程。

不要把全体用户塞进单个超大 Hash:它会形成热键,并让迁移、全量读取与删除成本变大。HGETALL 返回全部字段,只适用于边界清晰的小对象;大对象渐进使用 HSCAN。

4.2 List:有序序列

LPUSH lab:tasks job1 job2
LRANGE lab:tasks 0 -1
RPOP lab:tasks

LPUSH 参数从左向右依次插入,因此首次读取列表应是 job2, job1,右端弹出 job1。选择哪端入队和哪端出队决定 FIFO 或 LIFO,不能只根据命令名猜测。

BLPOP 等阻塞命令可以等待新任务,但弹出后消费者崩溃会丢失待处理状态。可以用 BLMOVE 把任务原子移到处理中列表,再实现确认、超时回收;需求更复杂时使用 Stream。列表并不是默认可靠的消息系统。

4.3 Set:去重与集合计算

SADD lab:tag:redis u1 u2 u2
SADD lab:tag:database u2 u3
SCARD lab:tag:redis
SINTER lab:tag:redis lab:tag:database
SISMEMBER lab:tag:redis u1

集合成员唯一且无序,第一条新增数量是 2,交集包含 u2。Set 适合标签、关注关系和访问资格。大集合交并运算会消耗主线程时间,跨槽也有额外限制。用线上全集临时计算报表,可能拖慢延迟敏感请求。

4.4 选择由访问模式决定

如果只需要完整文档读取且更新稀少,String JSON 很自然;如果经常读写单字段,Hash 更合适;如果需要顺序和重复值,用 List;如果强调成员唯一性和存在性判断,用 Set。不要只按节省几个字节选模型,要把读写频率、单键大小、并发集中程度和清理方式一起比较。

练习:评论列表为什么不一定适合一个无限增长的 List?给出分页、归档与删除的模型。为“一个用户的最近 20 次访问”设计有界列表操作,并说明两个独立命令之间可能出现什么短暂状态。

第 5 章 Sorted Set 与概率统计结构

5.1 排行需要分数与成员

ZADD lab:rank 100 alice 80 bob 100 carol
ZINCRBY lab:rank 15 bob
ZRANGE lab:rank 0 -1 WITHSCORES
ZRANGE lab:rank 0 1 REV WITHSCORES
ZRANK lab:rank bob

成员唯一,分数可修改。默认按分数升序,分数相同再按成员的二进制字典序;REV 会反向排序。因此若业务要求同分者先达到者领先,必须显式设计规则。不能声称 Redis 自动按照提交时间打破同分。

分数使用双精度浮点数,连续整数在绝对值不超过 2^53 时可精确表达。将时间戳与分数随意拼成很大的整数,可能损失精度。复杂排名规则可在应用中二次排序,或者给明确上限的组合编码写出范围证明。

5.2 范围查询与窗口

ZADD lab:events 1000 e1 1010 e2 1020 e3
ZRANGE lab:events 1005 1020 BYSCORE
ZREMRANGEBYSCORE lab:events -inf 1005
ZCARD lab:events

这种模型可用于滑动时间窗口:删除窗口外成员,添加当前事件,统计窗口内数量。但三个独立命令之间会被其他客户端插入,需要原子脚本;事件成员还必须唯一,否则相同成员会更新而非新增。窗口内每次请求都保存一条记录,内存随流量增长,不能忽略容量上限。

5.3 HyperLogLog:用误差换空间

PFADD lab:uv user1 user2 user1
PFCOUNT lab:uv
PFADD lab:uv:next user2 user3
PFMERGE lab:uv:all lab:uv lab:uv:next
PFCOUNT lab:uv:all

HyperLogLog 估计去重基数,不支持枚举原始成员,也不适合删除单个用户。标准误差约 0.81%,这是统计性质,不是“每次结果都在固定误差范围内”的确定保证。小规模示例恰好精确,不说明大规模也精确。需要精确结算时用有审计能力的精确数据源。

5.4 地理位置

GEOADD lab:places 116.397 39.908 palace 116.407 39.904 center
GEODIST lab:places palace center km
GEOSEARCH lab:places FROMLONLAT 116.397 39.908 BYRADIUS 2 km WITHDIST ASC

经度在前,纬度在后。GEO 面向近似地理范围查询,不是路网导航系统,返回距离也不是实际驾车距离。基础地理结构由有序集合编码支撑,不支持给单个地点直接设置键级 TTL。

练习:全球日活统计和付费用户账单分别应选择什么结构?如果排行榜必须让同分者按最早达成时间排序,列出一种不损失精度的方案及代价。

第 6 章 协议、连接与 Pipeline

6.1 一次响应的含义

客户端把命令编码为 RESP,通过 TCP 或 TLS 连接发送。服务器返回简单字符串、整数、错误、数组、空值等结果;现代客户端还可能使用 RESP3 的更多类型。业务代码应通过客户端库处理协议,而不是按换行手动切割字符串。

必须区分三件事:命令明确失败、命令明确成功、响应未收到。网络超时时命令可能已经执行。对 GET 重试通常无副作用;对 INCR、入队或扣库存盲目重试可能重复操作。请求 ID 与结果记录才是处理这种不确定性的关键。

6.2 连接池与超时

连接池限制应用对 Redis 的并发连接;过大的池可能把压力转嫁给服务器,过小则产生客户端排队。需要分别配置建连超时、读写超时和连接池等待超时,观察池等待与服务端延迟,而不是把全部时间归结为 Redis 慢。

阻塞式队列读取、订阅连接通常应使用独立连接,避免占住普通请求的连接。多进程应用要按实际进程数计算总连接预算。连接被借出后必须可靠归还;异常路径中的泄漏,常常表现为 Redis CPU 不高但应用请求排队。

6.3 Pipeline 减少往返

Pipeline 将多个命令连续发送,再批量读取响应,不需要等待每条结果后才发下一条。它不自动提供事务隔离;其他客户端仍可能在命令之间执行操作。某一条返回错误,也不会让先前已执行的命令回滚。

python3 -m pip install -r requirements.txt
python3 examples/pipeline.py

示例使用 pipeline(transaction=False),明确只做批处理。批次不应无限大,否则客户端、服务端输出缓冲区和网络都会承压。先从几百条有界小命令起步,根据数据体积和延迟分位数调节。

6.4 什么时候用什么

需求 工具 主要边界
批量获取几个值 MGET Cluster 同槽限制
多条独立写入减少往返 Pipeline 不保证整批原子性
原子执行固定命令组 MULTI/EXEC 不回滚运行时错误
依赖前序结果做条件更新 Lua 或 WATCH 脚本阻塞、冲突重试

练习:一个有 30 个应用进程的服务,每进程连接池上限 50,总连接上限是多少?若服务端 SLOWLOG 没有记录,但用户延迟很高,还应检查哪些环节?

第 7 章 事务、乐观锁与 Lua 原子性

7.1 MULTI/EXEC 保证什么

MULTI
SET lab:tx:a 1
INCR lab:tx:a
GET lab:tx:a
EXEC

MULTI 后命令排队,EXEC 时按顺序执行,执行过程中不会插入其他客户端的命令。它不提供传统关系事务的回滚。排队时的语法错误可能导致整个事务无法提交;执行时的类型错误则只影响相应命令,其他命令仍会执行。

SET lab:tx:bad text
MULTI
INCR lab:tx:bad
SET lab:tx:ok yes
EXEC
GET lab:tx:ok

最后仍读到 yes。因此应在事务前进行参数与类型设计,不要依赖“出错后自动恢复到事务前”。

7.2 WATCH 检查并发变化

客户端 A 执行 WATCH lab:balance,再读取余额并计算新值;客户端 B 在 A 提交前修改同一键;A 的 EXEC 将返回空结果,表示事务因监视键变化而未执行。过期等变化也会影响 WATCH。

应用需要重新读取、重新计算并有界重试,不能拿旧计算结果无限提交。竞争激烈时,乐观锁可能持续冲突;简单条件修改更适合脚本。

7.3 首次计数时设置 TTL

以下脚本放在 examples/fixed_window.lua。只操作通过 KEYS 显式传入的键,并在写入前验证参数。TTL 限制为 1—604800 秒,避免超大参数在计数写入后才触发 EXPIRE 范围错误。

local ttl = tonumber(ARGV[1])
if not ttl or ttl < 1 or ttl > 604800 or ttl % 1 ~= 0 then
  return redis.error_reply('ttl must be an integer between 1 and 604800')
end
local n = redis.call('INCR', KEYS[1])
if n == 1 then
  redis.call('EXPIRE', KEYS[1], ttl)
end
return n

这是“从首次请求起计时”的固定窗口计数,不是按自然分钟对齐,也没有消除窗口交界突发。调用者把计数与阈值比较决定是否放行,拒绝请求也会计入本次计数。运行方式:

docker compose exec -T redis redis-cli EVAL "$(cat examples/fixed_window.lua)" 1 lab:limit 60

脚本运行期间其他命令不能插入,长循环会阻塞整个实例。脚本发生运行时错误也不会回滚先前写入,因此应把可预见的校验尽量放到第一次写入之前。

7.4 脚本缓存与部署

EVALSHA 可减少传输脚本文本,但脚本缓存可能因重启、切换或其他原因不存在。客户端需要处理 NOSCRIPT 并按库的机制加载。Redis Functions 自 7.0 提供另一种服务端代码管理方式,但它的部署、命名和权限仍需设计。

练习:解释 Pipeline、事务和 Lua 的区别。若脚本先 DECR 再检查参数,检查失败抛错,库存会恢复吗?如何重排步骤?

第 8 章 过期、淘汰与内存容量

8.1 过期和淘汰解决不同问题

过期表达数据的有效期限;淘汰在达到内存限制时寻找可移除的数据。一个还没到 TTL 的键也可能因为淘汰被移除。因此数据库语义必须明确:丢掉一个键时能否重建?若不能,不能随意使用可淘汰策略。

Redis 通过访问时检查与后台主动过期处理移除到期键,不能保证物理内存在 TTL 到期的那个瞬间立即回收。键空间通知也不是可靠定时任务调度器:订阅者断开期间的事件不会补发。

8.2 选择 maxmemory-policy

策略 适合场景 需要接受的结果
noeviction 不允许自动淘汰的数据 部分写命令因内存不足报错
allkeys-lru 可重建的普通缓存 近似淘汰较久未访问键
allkeys-lfu 热点较稳定的缓存 近似按使用频率保留键
volatile-ttl 只淘汰设置 TTL 的键 无候选键时写入仍可能失败

LRU/LFU 是近似实现,不是精确全局排序。选择策略前先按数据角色拆实例;把缓存和不能丢的队列混在同一可淘汰实例里,可能产生不可恢复的丢失。

8.3 内存不仅是 value

INFO memory
MEMORY USAGE lab:user:42
MEMORY STATS
INFO stats

需要观察键元数据、对象编码、分配器开销、碎片、客户端缓冲区、复制积压和持久化额外开销。maxmemory 不等于进程 RSS 上限,部分缓冲区不计入淘汰比较;容器限额必须留出余量。RDB 或 AOF 重写期间的写时复制,也会随写入负载增加内存需求。

粗略容量估算:样本每条占用 500 字节,目标 1000 万条,仅该部分约 5 GB,再加其他结构与运行余量。不要用 JSON 字节数直接当实例需要的总内存;通过有代表性的样本在目标版本实测。

8.4 大键与热键

大键是体积或成员数很大的键,热键是访问量很高的键;两者可同时出现但不是一回事。前者影响传输、删除、迁移和恢复,后者会让单节点 CPU 或带宽饱和。拆分必须保留业务操作边界,不能把本来需要原子更新的数据随意散到不同槽。

练习:为什么给容器 4 GB 内存,设置 maxmemory 4gb 仍可能被 OOM 杀死?写出缓存与任务队列分实例的理由,并列出需要分别监控的指标。

第 9 章 RDB、AOF 与恢复演练

9.1 先定义损失预算

RPO 表示可接受的数据损失范围,RTO 表示可接受的恢复时间。选择持久化前先回答:能否丢最近一分钟的写入?能否停服十分钟重建?没有这些约束,很难评价某个配置是否“可靠”。

9.2 RDB 是时间点快照

RDB 将某个时间点的数据序列化到紧凑文件,适合备份、迁移和快速载入。后台快照通常通过 fork 子进程完成,父进程继续处理请求;fork 停顿、写时复制和磁盘负载仍可能影响线上延迟与内存。

BGSAVE
INFO persistence
LASTSAVE

BGSAVE 返回启动成功不代表快照已经完成。需要继续观察 rdb_bgsave_in_progress 与 rdb_last_bgsave_status。从上次成功快照之后的写入,若没有其他恢复来源,可能丢失。

9.3 AOF 是写操作记录

AOF 记录写操作,并通过重写压缩历史。appendfsync always 更频繁同步,成本更高;everysec 常用于性能与损失窗口的折中;no 依赖操作系统刷盘。everysec 通常描述约一秒量级的损失风险,不能当成任何磁盘阻塞、系统异常下的严格一秒上界。

Redis 7 采用多文件 AOF:基文件、增量文件和清单协同工作。不要只从目录里随便拷一个文件就声称备份完整。AOF 重写不等于“把所有写入停止后从头再来”,但后台过程仍会占用资源。

BGREWRITEAOF
INFO persistence

实验 Compose 开启 AOF everysec 并保留周期 RDB。两者均开启时,启动通常优先使用 AOF 恢复,因为它可能更完整。实际文件目录与配置必须一起核对。

9.4 恢复是备份的验收

一个可执行的恢复流程包括:记录版本与配置;确认快照/重写完成;按官方多文件 AOF 备份规则获得一致的文件集合,或在隔离教学环境停止实例后复制完整数据目录;为备份计算校验和;在新目录和独立端口启动兼容版本;核对数据与 TTL;记录耗时。

docker compose restart redis 只验证该卷的正常重启,不能证明离线备份可用,也不能证明断电损失上界。故障演练要区分正常停机、进程崩溃、主机掉电与磁盘损坏。

练习:如果副本实时复制了错误删除,它能作为历史备份吗?为什么 RDB 文件的存在不足以证明可恢复?制定一次包含数据核对和耗时记录的恢复演练。

第 10 章 复制、延迟与故障后的数据

10.1 异步复制的含义

主节点处理写入,并把复制流发送给副本。副本可能落后;从副本读取可获得旧值。写入主节点成功返回,只能证明该次操作在主节点所提供的语义下完成,不能自动证明所有副本都已收到并持久化。

首次同步可能传输完整数据;短暂断连后,若复制积压中仍有需要的历史,可部分同步。积压大小应根据写入速率和预期断连时间规划,不能把默认值当作适合所有环境。

10.2 观察复制

INFO replication
ROLE

关注角色、链路状态、复制偏移和副本延迟。在独立副本实例上使用 REPLICAOF host port 建立复制前,要确认它的数据可以被主节点数据替换。不要把实验命令运行到已有业务副本上。

主从延迟不只来自网络:副本磁盘压力、慢命令和主节点积压也会参与。读写分离的收益需要和一致性要求一起看;刚修改后的订单状态通常不应无条件从落后副本读取。

10.3 WAIT 不是共识协议

WAIT 1 1000 等待当前连接此前写入被指定数量的副本确认,返回实际确认的副本数。它减少部分故障下丢失的概率,但不把 Redis 变成强一致的共识数据库,也不自动保证已确认数据落盘。超时返回的数量不足,不表示此前写操作被撤销。

SET lab:replicated value
WAIT 1 1000

单实例实验中没有副本,通常返回 0,键仍然已被写入。Redis 7.2 的 WAITAOF 面向 AOF 同步确认,是不同层面的能力;是否能满足业务承诺仍取决于故障模型、选主与存储系统。

10.4 限制不等于保证

min-replicas-to-write 与 min-replicas-max-lag 可在健康副本不足时拒绝部分写入,牺牲可用性来降低风险,但并非每个写入的强一致提交协议。发生网络分区后,旧主和新主之间的结果仍要按实际切换过程分析。

练习:客户端收到主节点写成功后主机立即失联,新主为什么可能没有该值?如果下单后必须立刻看到新状态,描述一种读路由方案及其失败处理。

第 11 章 Sentinel:发现故障与自动切换

11.1 Sentinel 管理的是主从组

Sentinel 监测主副本、提供主节点发现,并在满足条件时协调故障转移。它不负责把一个键空间切成多个分片,也不让应用使用旧主地址就自动找到新主。客户端必须支持 Sentinel 发现并在切换后重新建立到当前主节点的连接。

生产通常部署至少三个 Sentinel,分布到独立故障域。教学配置形状如下,不是可以不改就用于生产的完整文件:

port 26379
sentinel monitor book-primary 10.0.0.10 6379 2
sentinel down-after-milliseconds book-primary 5000
sentinel failover-timeout book-primary 60000
sentinel parallel-syncs book-primary 1

其中地址须换成所有参与者可达的地址,认证、文件可写性和网络暴露也需配置。

11.2 quorum 与多数派

单个 Sentinel 认为主节点不可达是主观下线;达到配置的 quorum 可形成客观下线判断。真正开始故障转移还需要取得多数 Sentinel 的授权。quorum=2 不表示任意两台 Sentinel 在任何网络分区里都能选主。

在三个 Sentinel 中,两个能互相通信通常可以形成多数。把三个进程都放在同一台机器上,只提高了进程数量,没有提供主机级容错。探测超时越短也不一定越好,它可能把瞬时抖动放大成不必要的切换。

11.3 切换期间的应用

应用可能遇到连接断开、只读错误、超时或短时不可用。需要有界退避、重新发现主节点,并根据命令性质决定是否重试。重试会增加请求量,应配合熔断和整体超时预算。

故障切换与业务补偿必须同时设计。异步复制可能丢失旧主上已确认但未复制的数据;旧主恢复后通常会重新加入当前拓扑,不能把它当成另一份仍可随意写入的主数据库。

11.4 演练验收

演练前记录写入探针的递增序号;隔离主节点或在测试环境停止它;观察故障检测、选举、副本提升和客户端恢复;对比新主序号,记录不可用时间与缺失范围。仅看到日志“failover completed”,不足以证明业务恢复。

练习:为什么需要同时检查 quorum 和多数派?若客户端配置了固定主节点 IP,却没有 Sentinel 支持,自动切换后会发生什么?

第 12 章 Cluster:槽、路由与多键边界

12.1 槽是分片单位

Redis Cluster 将键映射到 16384 个哈希槽,由多个主节点持有槽,副本提供故障接管能力。槽数量不是节点数量,也不代表需要 16384 个进程。扩缩容通过迁移槽和数据完成,期间会有额外流量、延迟和路由变化。

常见教学拓扑是三主三副本,但进程数量本身不能证明跨故障域可用性。生产规划还要检查节点可达性、集群总线端口、客户端地址公告和认证配置。

12.2 同槽是多键操作的前提

book:{order:42}:detail
book:{order:42}:items
book:{order:42}:status

有效 hash tag 中花括号内的部分参与哈希,上面的三个键落到同槽,适合在事务或脚本中一起操作。普通的 book:order:42:detail 和 book:order:42:items 不会仅因为前缀相似就同槽。

hash tag 设计是一种局部性选择:所有库存都使用 {stock} 虽方便跨商品操作,却把请求集中到一个槽。按商品使用 {sku:42} 能分散负载,但跨商品原子预留就无法靠单个脚本轻易完成,需要上层协调与补偿。

12.3 MOVED 与 ASK

MOVED 表示槽的稳定路由归属,应更新槽缓存;迁槽期间的 ASK 是临时重定向,按协议发送 ASKING 后执行本次命令,不能简单把它当永久路由。选择成熟的 Cluster 客户端,避免自行手写重定向循环。

redis-cli -c -h 127.0.0.1 -p 7000

此命令要求另行启动真实 Cluster;本仓库默认 Compose 是单实例,不会在 7000 端口提供集群。单实例上执行相应跨键示例成功,不足以证明 Cluster 兼容。

12.4 扩容不能消除单键热点

一个键只属于一个槽,增加节点不会自动分摊它的每一次请求。可采取本地短期缓存、请求合并、读副本或业务分桶;每个方案都会改变一致性或聚合成本。先用数据确认瓶颈是在单键、单槽还是整个集群。

练习:为什么一个包含十万用户的超级 Hash 不能靠增加 Cluster 节点拆开?为单商品预留库存和多商品订单分别说明原子边界与补偿方案。

第 13 章 缓存一致性与失效策略

13.1 Cache Aside 的基本流程

读取时先查缓存,未命中再查数据库,并把结果写回缓存。更新时先提交数据库,再删除相应缓存,是常见的 Cache Aside 实践。数据库是事实来源,缓存是可以重建的加速层;应先把这个责任说清楚。

更新数据库后删除缓存仍存在失败窗口:数据库提交成功而缓存删除失败,就会继续读到旧值。可通过事务消息、可靠事件表(outbox)或变更订阅驱动重试,并用 TTL 限制残留时间。TTL 给出的是在特定刷新规则下的陈旧窗口,不能证明强一致。

13.2 两个并发请求的反例

初始缓存没有数据。请求 A 从数据库读到旧版本 v1;请求 B 把数据库更新为 v2 并删除缓存;A 随后把 v1 回填到缓存。即使 B 按“先更新再删除”执行,缓存仍可能回到旧值。

延迟双删可以减少部分竞态发生概率,但延迟选择依赖执行时间与网络,无法作为严格正确性证明。需要更强约束时,可按实体串行化变更和回填、使用有序版本及拒绝旧写的协议,或直接让关键读取走数据库。版本保护本身也要处理缓存不存在、淘汰和故障恢复后的版本信息丢失。

13.3 空值、热点和雪崩

缓存不存在的业务对象可缓解穿透,但空值必须和“缓存故障”分开表示,并设置较短 TTL。新建对象后要失效空缓存。Bloom Filter 是另一种优化,需要维护更新与误判语义;它不属于本书 7.2 基线的普通基础类型,不能直接假定所有实例都有 BF.* 命令。

热点键到期时大量请求同时回源称为击穿,可用进程内请求合并、互斥回填或提前刷新缓解。许多键同时失效、节点故障导致大面积回源,可能形成雪崩;TTL 随机扰动、分级缓存、限流和降级共同控制压力。

TTL 随机化示例:基础 300 秒,加上 0—60 秒均匀扰动。它可以打散整批写入导致的过期峰值,却无法解决 Redis 整体不可用时的回源峰值。

13.4 设计验收问题

写清命中率、回源 QPS、最大陈旧时间、缓存不可用时是否放行,以及数据库能否承受降级流量。对错误返回做区分:数据库确实不存在、Redis 超时和反序列化失败,三者不应都被视为业务不存在。

练习:用 A、B 两条时间线重现旧值回填。为一个商品详情接口定义可接受的陈旧时间,再设计数据库提交后失效失败的可重试机制。

第 14 章 分布式锁、租约与限流

14.1 锁首先是租约

SET lab:lock:report unique-owner-token NX PX 10000

条件写入加过期可获得一个租约。token 必须对每次持有者唯一,用于防止释放别人的锁。释放时不能先 GET 再 DEL,应在 Lua 中比较并删除:

if redis.call('GET', KEYS[1]) == ARGV[1] then
  return redis.call('DEL', KEYS[1])
end
return 0

当客户端暂停时间超过租约,另一个客户端可能已获得锁。原客户端恢复后继续写外部系统,仍可能造成冲突。给锁续期也不能从根本上消除进程暂停和网络分区带来的不确定性。

14.2 fencing token 的作用

如果下游资源能存储并拒绝旧序号,每次获得租约附带一个单调递增 fencing token,下游只接受更大的序号,就可以阻止迟到的旧持有者写入。关键在于下游实际验证,并且 token 的单调性在故障模型中成立;仅把随机 token 改成 Redis INCR,在异步复制故障切换下不自动获得全局单调保证。

需要严格互斥的资金或唯一性约束,应考虑数据库约束、事务或具有相应一致性保证的协调系统。Redis 锁很适合降低重复刷新,但不能把它包装成所有场景下的绝对互斥承诺。

14.3 三种常见限流

固定窗口实现简单,边界可突发;滑动日志按请求记录时间,较精确但内存随窗口请求量增长;令牌桶允许有界突发,用补充速率控制长期速率。选择方法时,应同时写出限额、窗口、突发容量和失败时策略。

分布式限流要把检查与更新放进一个原子操作。使用客户端时间需要处理时钟偏差;使用服务端时间也要考虑系统时钟调整。键按用户或 API 分桶,配合过期控制基数。没有身份上限的攻击流量,可能让“限流键”本身成为内存问题。

14.4 故障策略

Redis 不可用时放行,系统可用性较高但后端可能受冲击;拒绝请求保护后端,但用户受到影响。可结合进程内保守限流和降级额度。策略应由业务风险决定,并在演练中验证,不能让客户端异常处理偶然决定系统行为。

练习:解释“安全释放锁”和“安全访问外部资源”的区别。为发送短信设计用户、手机号和全局三层限流,说明 Redis 故障时如何处理。

第 15 章 Pub/Sub、Stream 与可靠消费

15.1 实时广播与可恢复日志

Pub/Sub 将消息推送给当前在线的订阅者,断连期间的消息不会重放,适合允许丢失的在线提示。Stream 保存有序消息并允许按 ID 读取,消费组还提供待确认列表。二者不是把命令换个名字就能互相替代。

XADD lab:orders * order_id 42 status paid
XRANGE lab:orders - + COUNT 10
XGROUP CREATE lab:orders workers 0 MKSTREAM
XREADGROUP GROUP workers worker-a COUNT 10 STREAMS lab:orders >

首次执行创建组成功;重复创建同名组会报 BUSYGROUP,应用应明确处理。组从 0 建立,表示可读取此前已有消息;若从 $ 建立,则从建组时的末尾以后开始。

15.2 待确认与接管

XREADGROUP 返回消息时,它会进入该消费组的 pending 状态。业务处理成功后,用返回的实际消息 ID 调用 XACK:

XACK lab:orders workers <上一步返回的消息ID>
XPENDING lab:orders workers
XAUTOCLAIM lab:orders workers worker-b 60000 0-0 COUNT 10

尖括号是需要替换的说明,不是可直接执行的 ID。XAUTOCLAIM 可接管闲置足够久的 pending 消息。超时时间应覆盖正常处理时长,否则慢消费者和接管者可能重复处理同一业务。

15.3 至少一次需要幂等

消费者完成数据库写入后,在 XACK 前崩溃,消息会被再次处理。因此需要数据库唯一键或幂等结果表,以业务事件 ID 拒绝重复副作用。若先确认再处理,则可能丢失尚未执行的工作。只靠 Redis 消费组,无法自动让外部数据库写入实现恰好一次。

建议处理流程:读取消息;解析与校验;在数据库事务里写入事件去重记录及业务变更;提交成功后确认。失败则重试,超过次数转入死信并告警。重试次数、异常分类和死信重放都应有操作入口。

15.4 保留策略

Stream 不会因为确认就自动删除消息。需要使用有容量边界的裁剪或归档策略。过早裁剪可能让 pending 只剩引用而无法读取原始消息体,影响恢复。近似 MAXLEN ~ 是性能与长度精度的折中,不能把近似上限当作精确内存配额。

练习:解释 XACK、删除消息和业务提交的关系。如果消息处理成功但确认响应丢失,消费者下一次该如何保证不会重复发货?

第 16 章 性能测量与慢请求排查

16.1 从用户延迟开始

平均延迟会隐藏少量很慢的请求,至少观察 p50、p95、p99 和超时率。记录请求量、命令分布、输入输出字节数及错误率,才能区分负载变大与单次操作变慢。端到端延迟也可能来自应用线程池或连接池,与 Redis 命令执行时间不是同一个指标。

INFO commandstats
INFO clients
INFO stats
SLOWLOG GET 10
LATENCY LATEST

SLOWLOG 主要计量命令执行,不包含完整的网络传输和客户端排队。LATENCY LATEST 只有在相应监控阈值启用并产生事件时才有数据;空结果不能证明系统没有延迟。

16.2 由证据缩小范围

若 CPU 高,先看高频或高复杂度命令、脚本和大集合操作;若网络饱和,看大值返回、重复获取和输出缓冲;若内存接近限制,看淘汰、碎片、对象分布和持久化峰值;若磁盘或 fork 相关指标异常,结合持久化状态与宿主机监控。

MONITOR 会带来额外开销且可能暴露业务参数,不宜长期在线打开。键抽样、redis-cli --bigkeys 等扫描工具也有成本,要在明确范围和时间窗口内执行。

16.3 基准不是排行榜

docker compose exec redis redis-benchmark -t get,set -n 10000 -c 10 -d 128

这只产生一个受控的小基准,不能代表你的订单接口吞吐。连接数、数据大小、命中比例、TLS、持久化、Pipeline 和客户端所在网络都会改变结果。压测应在独立环境进行,记录软件版本、硬件、配置和命令,再结合真实业务分布回放。

找到吞吐拐点的方法是逐级增加并发,观察吞吐增长是否放缓、尾延迟是否突增以及错误率是否提高。不要把已经超时的大量请求剔除后,只报告成功请求的漂亮延迟。

16.4 优化的次序

先修正不受控的数据规模和算法,再减少往返与无效请求,然后调整连接、批次和部署资源。微调底层配置前,建立前后对比基线,并保留回滚方案。

练习:若平均延迟 1 毫秒而 p99 达 300 毫秒,为什么平均值不足以描述体验?列出一种能区分服务端执行慢与客户端排队慢的观测组合。

第 17 章 安全、权限与部署边界

17.1 先限制网络可达性

Redis 不应直接暴露在公共网络。绑定地址、防火墙、容器端口映射和云安全组共同限定访问范围。protected mode 是一层保护,不能代替网络隔离与身份认证。容器中监听所有接口与宿主机是否开放公网端口,是不同配置层面。

本仓库仅映射 127.0.0.1:6379,不配置生产凭据。它适合本机实验,不是公网部署模板。共享机器上的其他本机进程仍可访问该端口。

17.2 ACL 按最小权限分工

Redis 6 起可用 ACL 创建独立用户,限制命令类别和键模式。下面是配置形状示例,必须替换口令并按目标版本检查命令权限:

ACL SETUSER book-reader reset on >REPLACE_WITH_STRONG_SECRET ~book:cache:* +get +mget +ping

示例不是 shell 命令;若在 shell 中直接粘贴 > 会被解释成重定向。即使在 Redis 交互终端运行,也应意识到口令可能进入终端历史。实际交付宜使用安全配置文件或密钥注入,避免把明文口令写入 Git。

读写用户、管理用户和监控用户应分开,管理命令不授予普通业务。脚本所调用的命令也受 ACL 规则约束;不要假设允许 EVAL 就获得所有能力,也不要仅靠键名前缀代替正确的多租户安全设计。

17.3 TLS 与敏感信息

TLS 保护传输中的身份凭据与业务值,并要求验证证书。仅开启加密而不校验证书,仍可能连接到错误服务端。生产应评估 TLS CPU 成本并配置证书轮换。

备份、AOF、慢日志、调试日志和故障截图都可能包含敏感信息,需要访问控制、保留策略与加密。把密码从代码移到环境变量只能解决一部分问题;进程环境、日志输出和 CI 配置仍需管控。

17.4 升级与变更

升级前核对发行说明、客户端支持、持久化格式兼容和模块依赖;先在恢复副本或预发布环境验证。滚动操作需要遵循具体拓扑的升级流程,不能简单地逐台重启并假设无损。回滚计划应包含旧版本是否能读取新版本生成的数据文件。

练习:为只读商品缓存设计最小 ACL 需求。为什么把 Redis 放在私有网络后仍需要认证?列出仓库与 CI 中必须避免记录的三类秘密。

第 18 章 监控、故障处置与恢复手册

18.1 指标应连接到业务后果

监控至少覆盖业务错误率与尾延迟、连接数与拒绝连接、内存与淘汰、复制延迟、持久化失败、主从角色变化以及磁盘可用空间。计数器需要换算为速率,节点重启后计数归零要正确处理。一个固定“命中率低于 90%”阈值并不适合所有缓存。

缓存命中率常用命中数除以命中与未命中之和,但指标只说明相应命令在 Redis 端的键访问;不一定等于应用完整缓存链路的命中率。进程内缓存、批量调用和业务空值会改变解释。

18.2 内存告警处置

先确认是数据增长、输出缓冲、复制/重写压力还是碎片;检查 used_memory、RSS、mem_not_counted_for_evict 和配置。确认淘汰速率及写入错误对业务的影响。若必须清理,先找明确的可重建命名空间,以限速 SCAN 和有界删除执行;不要临时使用全库清空来碰运气。

18.3 延迟升高处置

先保护业务:降低非关键流量、暂停高成本批处理、启用既定降级。保存指标时间线、慢日志和变更记录。排查慢脚本、大键操作、fork、磁盘同步、宿主机争用及连接池等待,再做有针对性的改动。

重启会丢失部分诊断信息,并可能触发冷缓存洪峰或全量复制。只有在明确收益、风险和恢复能力时才把它作为处置手段。

18.4 备份恢复验收表

项目 应留下的证据
来源 节点、版本、配置与备份时间
完整性 文件集合及校验和
隔离恢复 新实例端口、启动日志与角色
数据核对 关键样本、数量、业务不变量、TTL
时间 从开始到可提供业务服务的实测耗时
后续 备份加密、访问控制、过期清理

恢复不仅看键数量,还要检查余额非负、幂等记录与业务结果相符等不变量。有些到期键在恢复后自然消失,数量变化需要结合备份时间解释。

练习:为“淘汰速率突然升高但请求量没变”写一个排查次序。如何证明一次恢复演练满足 15 分钟 RTO,而不只是进程在 15 分钟内启动?

第 19 章 综合实战:幂等库存预留

19.1 范围与不变量

我们实现单商品、单实例原子预留:给定请求 ID 和数量,在库存足够时扣减一次;相同请求与数量的重试返回原结果;同一请求 ID 改数量应报冲突。任意成功预留之后库存都不能为负。示例代码在 examples/reserve.py 和 examples/reserve.lua。

这不是完整电商订单系统:没有支付、取消、持久账本或跨商品原子性。它的价值是把可验证的不变量放到清晰边界内。生产中数据库通常保存最终订单与库存事实,需要 outbox、补偿或其他一致性协议协调 Redis 与数据库。

19.2 数据模型

库存键 book:{sku:demo}:stock 保存非负整数;请求键 book:{sku:demo}:request:abc 保存请求数量及结果,默认保留 24 小时。相同 hash tag 让同商品键处于 Cluster 同槽,但示例实测环境是单实例,不声称已完成集群故障验证。

请求 ID 必须由调用方在重试之间保持稳定。客户端每重试一次就生成新 ID,幂等协议就失去作用。24 小时之后记录过期,旧 ID 可能再次生效,所以保证仅在保存窗口内成立;记录还会受到淘汰、持久化与复制策略影响。

19.3 原子流程

脚本先检查数量范围、TTL、库存与请求记录的类型,验证参数可解析;若存在请求记录,核对数量并返回旧结果;否则读取库存,充足就扣减,不足就返回拒绝;成功和拒绝都会写入有 TTL 的结果记录。

拒绝也幂等:同一请求因库存不足被拒绝后,即使补货,重试仍返回原拒绝。用户发起新的业务尝试时应使用新的请求 ID。这避免同一请求在不同重试中悄悄改变含义。

示例把数量和库存限制在 0 到 10 亿的整数范围内,让 Lua 数字比较与整数字符串处理保持清楚。更大整数不能直接依赖 Lua 双精度数字的精确性。脚本发生不可预见的服务端错误仍无回滚,因此隔离实例采用 noeviction 并预留内存,生产还需故障补偿。

19.4 运行与预期

python3 -m pip install -r requirements.txt
python3 examples/reserve.py --reset

--reset 只清理本示例专用的 book:{sku:demo}: 命名空间,再初始化 10 件库存。示例第一次请求 3 件后剩余 7;重复相同请求仍剩 7;更换 ID 请求 8 件被拒绝;原 ID 改成 4 件触发冲突。

python3 -m unittest discover -s tests -v

集成测试使用独立随机命名空间,覆盖幂等重试、数量冲突、库存不足、无效参数、TTL 和并发竞争。测试不会清空整个 Redis。并发测试的目标是验证“累计成功扣减不超过初始库存”,不是跑出最高吞吐。

19.5 从教学到上线

应用需要区分确定拒绝、参数错误和网络结果未知。网络结果未知时以相同请求 ID 重试或查询,不能换 ID 扣第二次。还需定义请求记录保留时间、库存初始化权限、库存变化审计与故障恢复对账。即使 Lua 保证当前主节点上的原子执行,异步复制切换仍可能丢失最近状态。

练习:如果请求结果键被提前淘汰,会发生什么?若要支持取消预留,应如何记录状态并防止同一取消重复加回库存?先画出状态机,再决定脚本接口。

第 20 章 练习参考答案与学习检查

20.1 基础与建模(第 1—5 章)

第 1 章:键不存在通常返回空值;连接失败是异常;类型错误由服务器返回错误。把它们统一成空值会隐藏服务不可用或模型错误,甚至让应用错误创建重复记录。

第 2 章:INCR 保留原 TTL;普通 SET 清除 TTL;SET ... KEEPTTL 保留已有 TTL。SCAN 需要游标走到零才能完成,批次上限限制负载,可重复执行处理重复返回及中途失败。

第 3 章:两个客户端可能同时读到不存在,随后都 SET。SET NX 把条件与写入合在一起。超时不能证明没有递增,计费类操作需要稳定请求 ID 和可查询结果。

第 4 章:无限 List 让范围读取、迁移和长期保留成本失控。可以按时间分桶,把完整评论保存在数据库,以游标分页访问,Redis 只缓存有限的最新评论。有界最近访问可在脚本中执行 LPUSH 与 LTRIM;独立执行时短暂超过上限,其他读者可能看见额外记录。

第 5 章:日活可以用 HyperLogLog 估计,账单应使用精确、可审计数据。排行榜可先按整数分数分组,再在同分组维护时间排序,或将有界分数与时间编码进经过精度证明的组合值;代价是读取与更新更复杂。

20.2 原子性与可靠性(第 6—12 章)

第 6 章:30×50=1500 条连接,还需加运维、监控和其他应用。排查应加入应用排队、池等待、网络 RTT、请求体积和服务端执行指标。

第 7 章:Pipeline 减少往返;事务执行命令组时不插入其他客户端命令,但不回滚运行时错误;Lua 支持条件逻辑并原子执行。脚本先扣减后抛错不会自动恢复,应前置校验并设计错误恢复。

第 8 章:RSS 可能超过参与淘汰比较的内存,fork 写时复制和缓冲区都需要余量。缓存可重建,队列通常不能丢,混用淘汰策略会让任务消失。

第 9 章:副本也会复制误删,不能代替历史备份。文件存在未证明文件完整、版本兼容与业务数据正确;必须在隔离环境恢复并计时。

第 10 章:异步复制未完成时旧主丢失,新主可能没有最后写入。写后短期固定走主节点可减少陈旧读取,但切换时仍需按业务一致性策略处理未知结果。

第 11 章:quorum 用于故障判断,多数派用于选举授权。固定旧主地址的客户端可能断连、继续访问旧主或遇到只读错误,需要拓扑发现。

第 12 章:Cluster 按键分槽,不会把 Hash 的字段拆到多个节点。单商品可同槽脚本,多商品需要数据库事务或业务协调,部分成功后有幂等补偿。

20.3 应用与运维(第 13—19 章)

第 13 章:A 读 v1,B 写 v2 并删缓存,A 回填 v1,即可重现旧值。outbox 将数据库业务变更与待处理失效事件放在同一数据库事务中,由后台可靠重试删除,消费者必须可重复执行。

第 14 章:比较 token 释放只防止误删他人的锁,不能阻止租约已失效的旧持有者写下游。短信限流可分别按用户、号码和全局预算执行,Redis 故障时采用保守本地限流或明确拒绝策略,避免无上限放行。

第 15 章:业务提交是外部状态变更,XACK 只是消费组确认,不自动删除消息。数据库以事件 ID 建唯一约束,在同一事务内做去重及业务写入,重复消息只确认、不重复发货。

第 16 章:平均值掩盖长尾;应用分段计时与服务端慢日志结合,能区分排队、网络和执行。压测应保留超时请求与失败率。

第 17 章:读缓存只需相应键模式及 GET/MGET 等命令,认证防止内部横向访问与误操作。口令、令牌、含隐私的业务值不应进入仓库和公开日志。

第 18 章:检查对象变大、TTL 被清除、批量导入、内存上限变化和缓冲开销。RTO 应计到应用恢复服务并通过业务探针的时刻,不只计算 Redis 进程启动时间。

第 19 章:幂等键提前丢失会让相同 ID 再次扣减。取消可设计 RESERVED→CANCELLED 状态转换,只在首次有效转换时加回库存;并发支付与取消需统一状态约束,保留完整审计来源。

20.4 自测清单

能否解释一条命令的时间复杂度及其数据规模?能否指出一个成功响应在复制与持久化上的边界?能否给每个键说明所有者、TTL、最大大小与失效方式?能否在网络超时后安全重试?能否用一次隔离恢复演练证明备份有效?这些问题比记住更多命令更接近生产能力。

附录 官方资料与命令速查

资料使用方法

本书以官方命令语义和运维文档为依据。以下链接指向持续更新的资料,访问日期为 2026-10-20;它们可能包含超出 Redis 7.2 基线的新增选项。使用前检查页面中的版本标记、目标服务版本和客户端支持,不要直接复制最新选项到旧实例。

主题 官方入口 对应章节
数据结构 https://redis.io/docs/latest/develop/data-types/ 3—5、15
命令检索与复杂度 https://redis.io/commands/ 全书
过期语义 https://redis.io/commands/expire/ 2、8
SCAN 保证 https://redis.io/commands/scan/ 2
Pipeline https://redis.io/docs/latest/develop/using-commands/pipelining/ 6
事务 https://redis.io/docs/latest/develop/using-commands/transactions/ 7
Lua 脚本 https://redis.io/docs/latest/develop/programmability/eval-intro/ 7、19
内存淘汰 https://redis.io/docs/latest/develop/reference/eviction/ 8
持久化 https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/ 9
复制 https://redis.io/docs/latest/operate/oss_and_stack/management/replication/ 10
WAIT https://redis.io/commands/wait/ 10
Sentinel https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/ 11
Cluster 规范 https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec/ 12
分布式锁 https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/ 14
Stream https://redis.io/docs/latest/develop/data-types/streams/ 15
ACL https://redis.io/docs/latest/operate/oss_and_stack/management/security/acl/ 17
TLS https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/ 17

命令速查

目的 命令 使用前想清楚
条件缓存写入 SET NX EX 返回空值和超时不是同一种结果
批量读取 MGET / HMGET 返回体积、Cluster 槽约束
修改计数 INCR / HINCRBY 整数范围与重试幂等
游标遍历 SCAN / HSCAN 无快照、可能重复、游标到零
排行 ZADD / ZRANGE 同分规则与分数精度
回收大对象 UNLINK 异步释放积压
原子条件逻辑 EVAL / EVALSHA 前置校验、执行时长、缓存失效
观察复制 INFO replication 延迟和已落盘是不同概念
消费确认 XACK 不替代外部业务提交
查慢命令 SLOWLOG GET 不代表端到端延迟

建议的学习记录

为每个实验记录五项:版本与配置、预期结果、真实输出、失败边界、业务用途。每次结论写出适用条件,例如“在单主当前状态上原子执行”,避免把局部观察扩展成所有故障模型下的保证。