第 1 章 引言 — 为什么是这三个软件
本章导读
熟练使用 Redis、MySQL 和 Kafka,并不意味着我们已经理解了它们的设计。比如,Redis 为什么采用单线程执行命令,InnoDB 为什么用 B+ 树组织主键索引,Kafka 的日志为什么采用追加写?这些设计各自解决了什么问题,又做了哪些取舍?如果换一种方案,结果会怎样?
我想以 Redis、MySQL 和 Kafka 为例,讨论它们面对过哪些约束、为什么这样设计,以及这些选择能给我们自己的系统设计带来什么启发。
这一章先讨论三个问题:为什么选这三个软件、它们各自是什么样,以及“架构视角”和“使用视角”看到的到底有什么不同。
1.1 为什么是这三个软件
在日常工作中,我几乎天天都要和这几个软件打交道。面对相同的问题,它们采用的方案却很不一样。我选它们做观察样本,就是想比较不同方案各自的取舍。
1.1.1 从三类常见需求说起
第一类是快速读写。缓存热点数据、计数器、实时对话,这些场景的共同诉求是低延迟、高并发。例如,请求命中热点数据的缓存后,就可以节约从原始存储读取的耗时;计数器和实时对话通常也在读取时更新数据,单次访问的耗时会增加请求的响应时间。
第二类是可靠存储。订单、账户、交易记录,这些数据一旦产生就不容丢失、不容错乱,还要支持复杂查询和事务。对这类能力,一致性和持久性是硬性要求。
第三类是异步解耦与流转。系统之间的通知、事件的分发、日志的汇聚、数据的搬运,都需要一个中间介质把生产者和消费者解耦。这类能力的关键在吞吐和可扩展性。
这三类能力是互补的。一个典型的电商订单系统常常会同时用到三者,比如 Redis 来保存购物车与库存计数、MySQL 保存订单与账户、Kafka 把“订单已创建”这个事件广播给履约、推荐、风控等下游。
本书以 Redis、MySQL、Kafka 这三个成熟的软件为观察样本,分别关注驻内存数据结构、关系事务处理和分区日志。图 1-1 给出它们在上述订单系统中的分工。
1.1.2 为什么选择这三个样本
在内存数据结构存储领域,Redis 是最广泛使用的实现之一,独到之处是“数据结构服务器(data structure server)”这一抽象:一套结构化类型加原子命令。
MySQL 是全球使用最广的开源关系型数据库之一。它的 InnoDB 引擎组合了事务、MVCC(多版本并发控制,Multi-Version Concurrency Control)、B+ 树聚簇索引、WAL(预写日志,Write-Ahead Log)等机制。更值得关注的是它如何随需求变化而调整设计:从早期的 MyISAM 到 InnoDB 默认化,再到组复制(MGR,MySQL Group Replication),每次迭代都在回应真实需求,也呈现了关系库在“怎么把数据存对”上的取舍。
Kafka 从 LinkedIn 内部的日志采集系统起步,逐步演化为一个分布式流平台。LinkedIn 2014 年公开的基准里,用 3 台廉价机测得约 200 万次写入/秒(当时的 Kafka 开发主干、100 字节消息),作为历史参考足够了。它把整个系统构建在追加写日志(append-only log)之上,这种设计影响了后面的 Pulsar、各种事件溯源(Event Sourcing)方案和 CDC(Change Data Capture)管道等一大批系统。
1.2 先认识这三个软件
正式对比之前,先介绍三个软件的出身、设计取向与关键取舍。
1.2.1 Redis:数据全部放在内存
Redis 的起点很朴素。2009 年,意大利开发者 Salvatore Sanfilippo(网名 antirez)在为自己的实时 Web 分析项目找一个能扛住高频访问统计的数据存储时,发现市面上的工具要么不够快、要么能力太单薄,于是干脆自己写了一个。
Redis 最有价值的贡献,是提出了数据结构服务器这一核心抽象。传统缓存如 Memcached 对外只暴露扁平的字符串键值,复杂操作要靠客户端读出来、改完再写回去。Redis 则走了相反的路:把字符串、哈希、列表、集合、有序集合、Stream 等结构化类型直接作为一等公民对外暴露,并为这些类型提供原子操作命令。比如对有序集合做 ZADD 和 ZRANGEBYSCORE,对列表做 LPUSH 和 BRPOP。这套抽象的好处是:一套原语就能支持 Redis 承担缓存、排行榜、消息队列、计数器、分布式锁这些角色,省去了在每个场景中重复实现这些能力的麻烦。
Redis 把全部数据放在内存中,以单线程执行命令;自 6.0 起,还可启用多线程 I/O 来加速网络读写。持久化靠 RDB(快照文件)与 AOF(Append-Only File,仅追加文件)两种方式互补。网络层采用事件驱动模型。内存和单线程的组合服务于低延迟与简单实现,RDB 和 AOF 则用于重启后恢复数据。
这种选择的代价:数据量受物理内存约束,若把 Redis 当成 MySQL 用,很快就会碰到内存上限。CPU 单核吞吐有上限,对一个键做重计算会卡住后面所有命令。复杂聚合与多表关联不是它的强项,要做就得把数据拉回应用层处理。这些短板是有意接受的:这些场景该交给别的系统,Redis 则专注于前面所说的低延迟与简单实现。
典型场景因此也高度集中在它的核心优势领域:缓存、分布式锁、计数器、排行榜、会话存储、轻量级消息。这些场景的共同点是访问频繁、结构清晰、对延迟敏感——1.1 节那个购物车与库存计数就是典型。
Redis 是为“快”和“简单”两个目标设计的内存数据结构存储。
1.2.2 MySQL:关系模型与事务存储
MySQL 的发展历史是一部商业与工程交织的演化史。它 1995 年起源于瑞典 MySQL AB,2008 年被 Sun Microsystems 收购,2010 年随 Sun 被 Oracle 收购后正式归入 Oracle 旗下。这个发展过程并不顺利:它经历过商业化收购的震荡、催生了 MariaDB 这样的分支,也经历了存储引擎从 MyISAM 到 InnoDB 默认化的更迭。但至今它仍是 Web 应用最常用的关系型数据库之一。
MySQL 的核心抽象是关系(表与行)+ SQL + 可插拔存储引擎。上层是统一的 SQL 接口与基于成本的查询优化器,对应用屏蔽底层差异;下层是 InnoDB 等可替换的引擎,真正执行存取。这种分层让 MySQL 既能提供标准化的访问方式,又能在存储层做差异化取舍。InnoDB 是默认引擎,本书讨论 MySQL 时以它为主,它把数据组织成页(page,默认 16KB),以 B+ 树聚簇索引为中心来管理整张表。
InnoDB 提供 ACID(原子性、一致性、隔离性、持久性)事务、靠 MVCC 实现的非阻塞读、行级锁。MVCC 利用 undo log(回滚日志)保留重建旧版本所需的信息,一致性读根据 Read View 判断版本是否可见:视图记录当时的活跃事务集合,读取时沿版本链从新到旧寻找可见版本。Read View 的生成时机与隔离级别有关:默认的 REPEATABLE READ 通常在首次一致性读时建立视图,后续复用;READ COMMITTED 则在每次一致性读时建立新视图。这类读不靠行锁阻塞写操作,而是读取可见版本,因此能与写操作并行。
InnoDB 维护着 redo log(重做日志,记录物理修改)与 undo log 两套日志,MySQL 服务层另有 binlog(二进制日志)用于复制与时间点恢复;数据访问先经过缓冲池(Buffer Pool),把热点页留在内存里减少磁盘随机 I/O。
优先保证数据正确性的代价体现在提交延迟和调优门槛上。在每次提交都刷盘的配置下,事务提交需要等待日志落盘,可通过 fsync 等机制把缓冲的写强制刷到磁盘。开启 binlog 时,redo log 与 binlog 还要通过内部两阶段提交协调:两份日志分属存储层和服务层,以 prepare、commit 阶段配合 binlog 写入,让崩溃恢复时能够判断事务的提交状态。
长事务持有的视图如果仍需要旧版本,相应的 undo 信息就不能清理,积累后可能拖累性能。调优门槛也高,缓冲池、刷新策略、隔离级别都需要根据负载仔细配置。此外,单机扩展有上限,纵向加内存、加 CPU 容易,横向分库分表则代价不菲。
MySQL 常用于交易系统,保存订单、账户等需要事务与复杂查询的业务数据。这些场景错不起:数据价值高、查询模式复杂,一笔错账的代价远高于一次慢查询。
1.2.3 Kafka:以“日志”为核心抽象的流平台
Kafka 的出身决定了它的设计取向。2010 年前后,LinkedIn 内部需要把海量的活动日志(用户行为、监控指标、审计流水)从各处采集出来,分发给下游的分析与处理系统。传统的企业级消息队列在那个规模下要么吞吐不够、要么运维太重。于是 LinkedIn 自己写了一套以“日志”为核心的系统,2011 年捐给 Apache,逐步演化为今天功能完备的分布式流平台。
Kafka 最核心的抽象,是追加写日志。一个主题(Topic)被切成多个分区(partition),每个分区本质上就是一条只追加、按偏移量(offset)寻址的有序日志。生产者只往尾部追加,消费者各自维护自己的偏移量向前读。这种“只追加、不修改、按偏移量定位”的模型看着朴素,但消息队列、事件流、数据管道、流处理都能用它实现。因为这些应用形态都需要有序事件持续流转。
Kafka 的日志以分区为并行处理单位,分区可以分散到多个节点。磁盘 I/O 走顺序写,配合操作系统的页缓存(PageCache)与未启用 TLS 时的零拷贝(sendfile),换来很高的单机吞吐。Kafka 还为分区日志保留副本(replica),用 ISR(In-Sync Replicas,同步副本集合)跟踪其中保持同步的副本。
随机查询不是 Kafka 的强项;批量等待可以用额外延迟换吞吐。消息默认按时间或大小保留;启用压实(compaction)时,可以用同一键的新值或删除标记表达更新与删除,再由后台清理旧记录。Kafka 优先保证吞吐与可扩展性。
压缩(compression)通过更紧凑的编码减少数据体积,解压后可以还原原始数据。压实则清理同一键的过时记录,不保证保留完整的变更历史。这两种机制可以同时使用。
所以 Kafka 用得最多的是高吞吐的流转场景:日志与事件采集、异步解耦、流处理管道、数据同步(CDC 的下游消费端)。这些场景的特点是数据量大、生产消费要解耦、能容忍一点延迟——还是 1.1 节那笔订单,履约、推荐、风控各自消费订单事件,彼此不必等待。
Kafka 是建立在日志抽象之上的高吞吐分布式流平台。
1.2.4 小结
表 1-1 从五个维度对比这三个软件的设计。
表 1-1 三个软件设计概览对比
| 维度 | Redis | MySQL | Kafka |
|---|---|---|---|
| 出身 | 2009,antirez 个人项目,为解决真实访问统计问题而写 | 1995,MySQL AB,经 Sun 入 Oracle,长期主流的关系库 | 2010,LinkedIn 内部日志系统,2011 捐 Apache |
| 核心抽象 | 数据结构服务器(结构化类型 + 原子命令) | 关系 + SQL + 可插拔存储引擎(InnoDB 为默认) | 追加写日志(分区 + 偏移量) |
| 本书重点观察 | 驻内存结构化操作的低延迟 | 事务处理与持久化 | 分区扩展与批量吞吐 |
| 主要限制与开销 | 容量受内存约束,复杂聚合能力有限 | 事务提交与日志维护开销 | 批量等待增加延迟,随机查询能力有限 |
| 典型场景 | 缓存、锁、计数器、排行榜、会话 | 交易、订单、账户、核心业务库 | 日志采集、异步解耦、流处理、CDC |
图 1-2 从低延迟、高吞吐、事务支持、数据容量、查询灵活度五个维度对比三个软件。事务支持关注提交、回滚与并发控制能力。
1.3 架构视角 vs 使用视角
同一个系统,从使用视角和架构视角去看,关注的重点不同。我们研究架构的核心方法是反复追问“为什么这样设计,反过来会怎样”,把视线从配置参数移到设计决策。
1.3.1 两种视角看到的同一个系统
面对“Redis 用单线程执行命令”这个事实,使用者关心的是怎样避免阻塞整个实例,例如避免执行 KEYS *,也避免对大集合做重计算。架构师追问的是:单线程在哪些负载下有优势、多线程 I/O 如何嵌入、这套设计在什么规模下就会吃力,以及换成多线程执行需要付出什么代价。
表 1-2 使用视角 vs 架构视角对照
| 关注维度 | 使用视角(怎么做) | 架构视角(为什么这么做) |
|---|---|---|
| 提问方式 | 怎么配?怎么调?踩过哪些坑? | 为什么这么选?反过来会怎样?边界在哪? |
| Redis 单线程 | 避免 KEYS *、避免重计算、压测能达到多少 QPS(每秒查询数) | 单线程在哪些负载下有优势?多线程 I/O 如何嵌入?瓶颈何时出现? |
| MySQL 日志 | 开 binlog、设 sync_binlog、配主从 | redo 与 binlog 为何要两阶段提交?日志作为数据正本,边界到哪为止? |
| Kafka 副本 | 设 acks=all、min.insync.replicas=2(ISR 至少有 2 个副本,并等待当前 ISR 全部确认) | ISR 为何要“动态收缩”?Leader 切换与一致性如何兼顾? |
| 知识形态 | 配方、参数、最佳实践 | 取舍、约束、演化逻辑、可迁移的设计思想 |
| 终极目标 | 把现有系统用好、用稳 | 理解设计决策,并在自己的系统里做对取舍 |
1.3.2 架构视角的三个方法
从架构视角分析系统,有三个方法:看约束、看演化、看抽象。
第一,看约束。 任何设计都是在特定约束下的取舍。要理解一个决策,先还原它面对的约束:目标场景是什么、性能要求多高、可靠性要求多严、维护成本能不能承受。Redis 的数据在内存中,许多命令的执行时间很短;用单线程执行命令,可以免去并行执行命令时的数据竞争和锁协调,重计算则会用满单核。MySQL 需要主索引支持范围查询和排序;磁盘随机访问又很昂贵,因此使用能批量读取相邻键的 B+ 树,而不是哈希索引。约束变了,同一个选择可能就不再合适。
第二,看演化。 架构是需求逼出来的。Redis 从单机到哨兵(Sentinel)再到 Cluster,是因为单机扛不住可用性与可扩展性的双重压力;MySQL 从 MyISAM 到 InnoDB 默认化再到 MGR,是因为业务对事务与高可用的要求不断抬高。Kafka 则是从依赖外部 ZooKeeper 走到内置 KRaft(Kafka Raft,Kafka 自带的共识元数据管理),原因是外部元数据依赖增加了运维负担,元数据管理方式也限制了扩展;KRaft 将这部分职责纳入 Kafka 自身的共识集群。理解“为什么变”,关键是看到约束在变,设计就得跟着变。今天看起来“不合理”的旧设计,往往只是被新约束淘汰了而已。
第三,看抽象。 复杂系统对外只暴露少数几个核心原语。Redis 暴露的是数据结构,MySQL 暴露的是关系与 SQL(页与索引是藏在引擎里的实现),Kafka 暴露的是追加写日志。抽象不同,应用组织和操作数据的方式也不同。
1.3.3 一个贯穿全书的例子
一个后端最经典的问题:保存数据怎么保证不丢?
架构视角需要把这个问题拆成三个层次的追问。 第一,先刷日志还是先刷数据页? 采用 WAL 的存储引擎要求:修改过的内存页写回磁盘前,对应日志必须先落盘。日志顺序追加,可以把数据页的分散写回延后;故障后再依据日志恢复尚未落盘的修改。 第二,日志何时落盘? 是每条命令落盘、每秒落盘,还是交给操作系统决定何时刷盘?更及时地落盘可以减少尚未持久化的数据,更频繁地刷盘则会增加开销。 第三,日志记的到底是什么? 这三个软件在这里分歧最大:记录执行过的命令、数据页上的修改,还是消息本身?
逻辑日志记录操作,重放时要重新执行或还原这些操作,也要处理重复重放和版本兼容;物理日志记录数据页等存储结构的修改,恢复与具体存储格式联系更紧。三个软件的日志内容和恢复方式各不相同。 Redis 的 AOF 是命令日志,即逻辑日志,记的是“执行了什么命令”,重放时重新执行这些命令。 MySQL 的 redo log 记录数据页上的修改操作(严格说是物理到页、逻辑到操作的混合日志),恢复时应用这些记录;binlog 则记录语句或行变更,用于复制。redo log 与 binlog 通过内部两阶段提交协调事务状态。 Kafka 把数据本身存成日志:消费者从日志里读,副本把日志复制过去。异常退出后,Broker 重启时恢复日志段和索引,必要时处理不完整的日志尾部,不需要把命令重放成另一套业务数据结构。
三种方案,一条共同主线:日志为故障恢复提供依据,刷盘与副本确认策略共同影响数据丢失窗口和吞吐。 这条主线贯穿第 4 章(内存与磁盘)与第 9 章(数据同步),并在后续多章里以不同的面貌重新出现。
1.4 如何读本书
1.4.1 谁应该读,前置要求是什么
本书面向三类读者。第一类是 2 年以上经验的后端开发者:你已经用过 Redis、MySQL、Kafka 中的至少一种,能把系统跑起来,但想搞清楚“为什么这样设计”。第二类是负责技术选型的架构师:你需要在多种方案之间做权衡,而权衡的前提是理解每种方案的设计取向与代价。第三类是技术负责人:你需要一套可以传授、可以复用的架构认知框架。
阅读需要基本的数据结构、算法和操作系统常识:对哈希表、树、跳表这些名词不陌生,能大致说清进程与线程、内存层次、I/O 模型(阻塞、非阻塞、I/O 多路复用)。你至少实际用过三者之一,知道客户端怎么连、命令或 SQL 怎么写。建议在阅读过程中充分使用AI工具来帮助你学习和回忆相关知识点。
1.4.2 本书的组织方式:主题式而非软件式
起初我按软件逐个整理,写完发现不行:共性讲不出来,Redis 的持久化和 Kafka 的持久化隔着好几章,对比不是很明显。改成按主题对比之后,三家的差异和背后的共性才同时浮出来。
所以,本书采用主题式的组织:每一章围绕一个架构主题,先分别看 Redis、MySQL、Kafka 在该主题下的实现,再做横向对比,并尝试挖掘架构启示。
下面这张图给出全书的章节结构。
每一章讨论的是同一个底层问题在不同软件里的不同实现,章与章之间是递进的关系。第 2–4 章讲系统内部的基础问题——数据用什么结构表示、进程启停怎么保住状态、内存与磁盘怎么分工。有了这个底子,第 5–7 章才谈系统的组织:层怎么切、谁能访问、容量怎么扩。第 8–9 章处理落盘与搬运:数据在磁盘上存成什么格式、副本之间怎么同步。
1.4.3 版本基线与阅读约定
凡涉及具体行为、参数或默认值的地方,都以这个版本基线为准:Redis 7.x、MySQL 8.0.x、Kafka 3.x(分层存储 Tiered Storage 相关讨论以 3.9+ 为准)。涉及版本差异的地方另作说明。全书信息核对截止 2026 年 9 月;Redis 许可证变更与 Valkey 分叉等新版本差异不在本书展开范围,请读者自行对照。
版本状态提示(2026 年 9 月):MySQL 8.0 已于 2026 年 4 月转入 Oracle Sustaining Support,官方建议升级至 8.4 LTS 或 9.7 LTS;Kafka 从 4.0 起只支持 KRaft,已移除 ZooKeeper 模式;Redis Open Source 的 7.2、7.4 分支仍列在官方支持范围内。本书保留上述分析基线,借 B+ 树、WAL、状态机复制、CAP 等主题讨论架构取舍;具体参数默认值和命令行为,请以所用版本的官方文档为准。
几处与版本相关的演进包括:Redis 在 6.0 引入了多线程 I/O 和 ACL(访问控制列表),7.x 又带来了 Function(函数)、Sharded Pub/Sub 等改进;MySQL 8.0.x 带来了原子 DDL、新版 redo log、降序索引、直方图,组复制(MGR)也在这一代走向成熟;Kafka 在 3.3 让 KRaft 达到生产可用(新集群自此可不依赖 ZooKeeper 部署),分层存储(Tiered Storage)在 3.9 正式 GA,而生产者幂等与事务早在 0.11 就已引入。
图表约定如下。图编号统一为 图 N-M(第 N 章第 M 张图),表编号统一为 表 N-M(第 N 章第 M 张表),图与表各自独立连续编号。
术语约定如下。全书术语统一译法,首次出现时中英对照标注,之后按两档制使用:工程界通行的术语直接用英文,如 redo log、undo log、binlog、WAL、MVCC;中文已充分沉淀的继续用中文,如缓冲池(Buffer Pool)、分区(partition)、副本(replica)、压实(compaction)。遇到未列出的术语,我们会在首次出现时同样标注英文原词。本书出现的 KIP-xxx 是 Kafka 社区改进提案(Kafka Improvement Proposal)的编号,用来标记某特性由哪份提案引入,不关心社区历史的读者可以跳过。
1.4.4 推荐阅读路径
本书提供三条阅读路径,按你的目标选择。
路径 A:系统通读。 从第 1 章顺序读到第 10 章。适合想建立完整架构认知的读者。按全书的主题顺序阅读,有助于连贯地理解后端基础设施的设计逻辑。这是最推荐的路径,也是本书写作时假定的阅读顺序。
路径 B:按需精读。 先读第 1 章和第 10 章建立全局观,再按工作场景跳读主题章。比如你正在排查持久化问题,直接看第 4 章;做高可用选型或优化存储成本,则分别对应第 7 章和第 8 章。这条路径适合已经有相当经验、只想补某一块认知的读者。
路径 C:对比驱动。 每章只重点看横向对比与架构启示(多数章在偏后的小节),再按兴趣回看具体实现。但前提是你对这三个软件已经有基本了解,否则对比看不出门道。
无论选哪条路径,每一章都可以这样读:先读导读、带着问题进去,读完再看小结收束全章。把每章的“架构启示”作为可参考的设计原则。
1.4.5 全书脉络
我最看重三个章节:第 2 章的数据结构与协议,是理解后面各章的基础;第 4 章“内存与磁盘”所处理的“快”与“不丢”这对矛盾,是存储系统的核心命题;第 9 章“数据同步”把前面讨论的副本、日志和一致性联系起来。第 10 章再回顾这八个主题章,提炼共性规律和取舍方法。
1.5 小结
这一章讲清了三个问题。为什么选 Redis、MySQL、Kafka 当样本:因为它们处理相近的数据问题,又采用了不同的数据模型和访问方式。它们各自是什么样:1.2 节的三段概览交代了各自的出身、抽象与代价。架构视角与使用视角有什么不同:使用视角问“怎么做”,架构视角追问“为什么这么选”。用“数据不丢”这个例子做三层追问(先刷日志还是先刷数据页、落盘粒度多细、日志记录什么内容),就是架构视角的样板。