读书笔记 · 约 11,123 字 · 预计推送 2 部分

精读笔记:Redis 设计模式——缓存、限流、分布式锁、队列与发布订阅的边界 未开始

目录

题记与说明


0. 来源清单(精读顺序)

#来源类型链接
1Distributed Locks with Redis(Redis 官方文档,2026-10-29 更新)官方文档https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/
2Martin Kleppmann: *How to do distributed locking*(2016-02-08)批评文章https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
3antirez: *Is Redlock safe?*(2016-02-09,Redlock 作者反驳)作者回应http://antirez.com/news/101
4Redis Streams(Redis 官方文档)官方文档https://redis.io/docs/latest/develop/data-types/streams/
5Redis Pub/sub(Redis 官方文档)官方文档https://redis.io/docs/latest/develop/pubsub/
6Redis cache-aside(Redis 官方文档)官方文档https://redis.io/docs/latest/develop/use-cases/cache-aside/
7Redis rate limiter(Redis 官方文档)官方文档https://redis.io/docs/latest/develop/use-cases/rate-limiter/
主题要求"检索 3–5 个来源",实际精读 7 个:锁这一条边界是整个生态里被辩论最充分的话题(官方 spec + 批评者 + 作者反驳构成完整对话),必须三方并读才有"边界";缓存与限流各补一篇官方 use-case 文档保证五个模式全覆盖。多读的 2 篇均为对主题的直接服务。

1. 核心问题

这批工作在解决什么问题:Redis 官方与社区用同一套原语(单线程原子命令、TTL、Lua 脚本、异步复制)搭出了缓存、限流、分布式锁、队列、发布订阅五类经典模式。真正的问题不是"命令怎么写",而是:每个模式到底提供了哪些保证(guarantees)、在哪些故障场景下保证会失效、失效时你付出的代价是什么——即五类模式的边界。

一个贯穿全部来源的判断标准由 Kleppmann 提出(对锁,但对五类模式普遍适用):先分清你要的是效率(efficiency)还是正确性(correctness):

"Efficiency: Taking a lock saves you from unnecessarily doing the same work twice... If the lock fails... the result is a minor increase in cost... Correctness: Taking a lock prevents concurrent processes from stepping on each others' toes... the result is a corrupted file, data loss, permanent inconsistency..."(来源 2)

五类模式全部共享同一组底层事实:单实例内命令原子(单线程)、键过期基于墙钟 gettimeofday 而非单调时钟、主从复制异步、故障转移是 best-effort。因此每条边界都由这组事实推导出来,而非独立的工程玄学。


2. 主要主张

2.1 总主张:Redis 五模式的"保证阶梯"

模式能给的保证给不了的保证边界越过即失效
缓存(cache-aside)亚毫秒读、TTL 有界陈旧、写时失效强一致;击穿时不打穿主库(需额外机制)把缓存当持久层 / 无 TTL / 无失效策略
限流集中式原子计数、亚毫秒决策精确公平(多键拆分有近似);主从切换瞬间计数可能丢用本地计数器打多实例
分布式锁效率锁(best-effort 互斥)正确性互斥(除非外接 fencing token / 共识系统)把 Redis 锁当正确性锁用
队列(Streams)持久化条目、消费者组、PEL+ACK、at-least-once跨机分区、强持久(异步复制);failover 可能缺消息当作 Kafka 级容错消息总线
发布订阅进程间实时扇出持久化、ACK、离线重放——at-most-once用 pub/sub 传"丢了会出事"的消息

2.2 锁:官方文档自己承认的边界(来源 1)

单实例正确姿势是 SET resource_name my_random_value NX PX 30000,解锁必须校验值(Redis 8.4 起可用 DELEX key IFEQ my_random_value,旧版本用 Lua):

"Using just DEL is not safe as a client may remove another client's lock."

主从失效转移在原理上就会破坏互斥:

"Client A acquires the lock in the master. / The master crashes before the write to the key is transmitted to the replica. / The replica gets promoted to master. / Client B acquires the lock to the same resource A already holds a lock for. SAFETY VIOLATION!"

Redlock 用 N=5 独立主节点 + 多数派把风险压低,但官方文档最后的 *Disclaimer about consistency* 直接给出两条自认边界:

"1. You should implement fencing tokens. ... 2. Redis is not using monotonic clock for TTL expiration mechanism. That means that a wall-clock shift may result in a lock being acquired by more than one process."

(fencing token 是针对所有带租约的锁系统的普适补丁,官方在这里采纳了 Kleppmann 的立场。)

2.3 锁:批评方的论证(来源 2 Kleppmann)

三个要点:

  1. 效率锁别用 Redlock:单实例足够,"documenting very clearly in your code that the locks are only approximate";
  2. 正确性锁别用 Redlock:GC 停顿/网络延迟可以让持锁者"回来后仍然自信地写入",唯一解是 fencing token——而 Redlock 不产生单调递增 token:
"However, this leads us to the first big problem with Redlock: *it does not have any facility for generating fencing tokens*."
  1. Redlock 的安全性依赖同步系统模型(有界延迟、有界停顿、有界时钟误差),并引用 GitHub 90 秒包延迟事故(2012)作为反例。

结论原句(本主题最著名的一句话):

"I think the Redlock algorithm is a poor choice because it is 'neither fish nor fowl': it is unnecessarily heavyweight and expensive for efficiency-optimization locks, but it is not sufficiently safe for situations in which correctness depends on the lock."

2.4 锁:作者的反驳与让步(来源 3 antirez)

"Whatever delay happens in the network or in the processes involved, after acquiring the majority we *check again* that we are not out of time."

三方交叉后的边界结论:效率锁 → 单实例 SET NX PX + 值校验解锁;正确性锁 → 要么外接 fencing token(并让存储侧校验 token),要么换 ZooKeeper/etcd 类共识系统。把"随机 token 做 CAS"也理解为一种 fencing(校验存在性而非序号大小),两派方案在工程上可以合并。

2.5 队列 vs 发布订阅:同一文档给出的分界线(来源 4、5)

官方 Streams 文档用一段话同时划出 List、Pub/Sub、Streams 三者的边界(本主题最重要的一段原文):

"While in Pub/Sub messages are *fire and forget* and are never stored anyway, and while when using blocking lists, when a message is received by the client it is *popped* (effectively removed) from the list, streams work in a fundamentally different way. All the messages are appended in the stream indefinitely (unless the user explicitly asks to delete entries): different consumers will know what is a new message from its point of view by remembering the ID of the last message received."

Pub/Sub 文档进一步把语义钉死:

"Redis' Pub/Sub exhibits *at-most-once* message delivery semantics... If the subscriber is unable to handle the message (for example, due to an error or a network disconnect) the message is forever lost."

并明确指路:"If your application requires stronger delivery guarantees, you may want to learn about Redis Streams. Messages in streams are persisted, and support both *at-most-once* as well as *at-least-once* delivery semantics."

Streams 的消费者组提供五条保证(交付互斥、具名消费者、last-delivered-id、显式 ACK、PEL 待处理追踪),配合 XCLAIM/XAUTOCLAIM 实现消费者死亡后的接管,交付计数器即内建死信判断依据。但 Streams 的持久化边界同样被官方写死:

"AOF must be used with a strong fsync policy if persistence of messages is important in your application. By default the asynchronous replication will not guarantee that XADD commands or consumer groups state changes are replicated: after a failover something can be missing... the Redis failover process as operated by Sentinel or Redis Cluster performs only a *best effort* check."

与 Kafka 的区别也有一句准话:"The partitions are only *logical* and the messages are just put into a single Redis key... Kafka partitions are more similar to using N different Redis keys, while Redis consumer groups are a server-side load balancing system."(想要真分区必须自己按 key 分片。)

2.6 缓存:有界陈旧而非强一致(来源 6)

2.7 限流:原子性由 Lua 兜底(来源 7)


3. 方法(本主题研究如何做)

  1. 检索:先 WebSearch 定位官方一手文档(redis.io),确保每个模式有官方 use-case/data-type 文档;锁主题额外定位 2016 年 Redlock 争论双方原文(Kleppmann 原文 + antirez 反驳),因为这是理解"边界"最完整的一手对话。
  2. 全文精读:用 webReader/WebFetch 抓全文(非摘要),重点读各文档的 *Safety and Liveness*、*Disclaimer about consistency*、*Delivery semantics*、*Persistence, replication and message safety* 这类"保证/局限"小节——边界几乎全部写在这些小节里。
  3. 交叉对照:官方文档(主张什么)× 批评者(主张何时失效)× 作者(接受哪些批评、反驳哪些),三者重叠部分即为高置信结论,分歧部分标注为未决。
  4. 七维提取:按核心问题/主张/方法/证据/结论/局限/启发整理成保证阶梯表 + 逐模式边界清单。

4. 数据与证据

5. 结论

  1. 五类模式共享一个根保证:单实例原子性。 所有跨实例的承诺(复制、failover、时钟)都退化为 best-effort。选型第一问不是"Redis 能不能做",而是"这个场景丢一次/重一次/慢一次会怎样"。
  2. 消息可靠性阶梯(丢消息的代价从高到低选型):

Pub/Sub(断线即丢)→ List 队列(弹出即拥有,进程崩=丢)→ Streams(持久化 + PEL/ACK/XCLAIM,at-least-once 可达,但 failover 仍可能缺)→ 外部多副本 MQ(Kafka 等,跨机持久化)。

  1. 锁的正确性边界:效率锁用单实例 SET NX PX + 值校验解锁即可,官方与批评方在这点上完全一致;正确性锁必须给资源访问加 fencing(单调 token 或 CAS token),否则任何带租约的锁(不止 Redis)都会被 GC/网络延迟击穿。正确性要求极高时换共识系统。
  2. 时间是所有模式的公共暗礁:锁的租约、缓存 TTL、限流窗口全部依赖时间语义,而 Redis 过期曾基于墙钟(antirez 已承诺并实现单调时钟改进)。时钟跳变对五个模式同时生效。
  3. 模式之间不是竞争而是分层:同一个 Redis 里常见组合是 cache-aside(读)+ 限流(保护)+ 锁(防击穿/防重复)+ Streams(事件分发)+ Pub/Sub(实时通知)——边界清晰后,组合反而安全。

6. 局限(作者承认的 + 我推断的)

7. 对我的启发

  1. "先问丢一次会怎样"是可迁移的选型公理:不止 Redis——任何"用 X 实现 Y"的第一问都是"Y 的哪条保证是硬需求"。这条公理直接适用于我自己的项目选型(缓存击穿防护用不用锁、用不用 Streams 替代 pub/sub 等)。
  2. 效率/正确性二分值得写进代码注释:Kleppmann 建议对效率锁"documenting very clearly in your code that the locks are only approximate"——把边界承诺写进代码,而不是留在口头。
  3. 边界信息藏在文档的 disclaimer 小节:官方文档的 *Safety and Liveness* / *Delivery semantics* / *Persistence* 小节比教程正文信息密度高一个量级;以后精读技术文档优先直奔这些小节。
  4. 时间是被低估的分布式变量:TTL、租约、限流窗口共享同一时钟;时钟跳变一次击穿全部。这解释了为什么"过期"类设计都要留时钟漂移余量(MIN_VALIDITY = TTL - 获取耗时 - CLOCK_DRIFT)。
  5. 随机 token + 资源侧 CAS 是弱环境的通用解:当没有单调序号可用时,"把 token 写进资源状态、改前校验"提供了与 fencing token 等价的互斥检查——这个模式可平移到任何自建锁/幂等场景。

*来源链接汇总:见 §0 表格。本笔记仅存于本文件,未改动同目录其他条目、matrix.csv、backlog 或 nightly-log.md。*

← 资料库今日播报 →