---
title: 数据库是个大单点
taxonomy:
    category: docs
---

在 Web 系统中，我们经常面临这样的需求：用户需要逐个注册，ID 不能重复；订单需要逐个处理，两个订单的信息不能混淆。这实际上就是最基本的需求——排队。无论数据库如何拆分，微服务如何设计，总有一个资源必须排队等待处理，即使我们可以使用“拿现在的时间换未来的时间”的方式来优化性能（后续章节将详细介绍），排队仍然是不可或缺的。

在常见的 Web 系统架构中，关系型数据库是最大的不可分割的单点：在一个系统中，多个 API 调用产生的多种行为最终都会作用于同一张表上，只有这样才能保证系统的正常运行。这也是数据库最核心的价值所在——API 可以并发执行，而数据库必须逐个处理请求。

和 Golang 协程使用 Redis 排队、Node.js 内置的单线程队列一样，数据库才是 Web 服务的“根”，是确保最终计算结果符合预期的那个“队列”。

无论进程、线程、协程如何并发执行，数据库的自增、事务和锁都是原子性的。这种原子化能力正是和数据库配合的业务服务器能够多台并发运行的基础，也是多台服务器能够被称为“一个系统”的逻辑基础：如果两个系统的数据库不在一起，那它们就不是同一个系统，就像拼多多有 7.5 亿月活跃用户，淘宝有 8.5 亿，你不能简单地说“拼宝宝”电商系统拥有 16 亿月活跃用户一样。

这种哲学思想我们将在最后一章的终极高可用架构中再次使用。

### 数据库为何成为单点

数据库之所以成为单点，并非是其自身所决定的，而是系统架构需要数据单点的存在，而数据库正是为了满足这一需求而设计的。对于大部分需要记录到数据库中的信息而言，按照先来后到的顺序进行排队入库是不可或缺的硬性要求，否则数据就会出现错误。而对于所谓的 NoSQL 数据库来说，即使是一个简单的 ID 自增操作，也需要扫描整个表才能实现，因此它无法承担数据单点的角色。由于 NoSQL 并非为承担数据单点而设计，它注定只能作为关系型数据库提升性能的辅助工具。

关系型数据库中的“关系”二字，指的是一个表中的数据之间存在着行和列两个方向的关系，这实际上是另一种形式的“时间换空间”、“空间换时间”的思想：通过提前约束这些数据，让它们以一定的规则存储在一起，虽然写入时会稍慢一些，但调用起来却简单高效，下面是几个简单的能力示例：

1. 无需全表扫描即可瞬间找到最大 ID；
2. 即使将海量数据存储在磁盘上，仍能以极快的速度检索满足某个条件的某一行；
3. 在快速定位到某一行后可以迅速提取连续多行的数据。

### 数据库是一个极其复杂的软件

关系型数据库如同空气般充斥在后端技术中，然而可能很少有人意识到一个惊人的事实：99.9% 使用 MySQL 的系统，其业务代码的复杂度远远不及 MySQL 内部的复杂度高。

为了实现关系型数据库的四大原则，MySQL 几乎将计算机的每一种资源都发挥到了极致：进程、线程、多核、网络、寄存器、内存、机械磁盘、固态磁盘。

至此，如果我们开始深入分析 ACID 实现的细节，岂不是陷入了俗套？我们要不走寻常路，不背面试八股文，直接剖析 MySQL 的底层原理。

### 数据库的单点，究竟在哪里？

在探讨持久性和原子性时，许多技术文章都会提到 undo log、redo log、多版本并发和锁等概念。然而，笔者对此持不同看法。让我们从 MySQL 的基础功能出发来思考。

MySQL 是整个 Web 系统中唯一一个能够在意外掉电重启后保持数据不丢失的组件。那么它是如何实现的呢？答案很简单，它依赖于计算机中唯一一个断电不丢数据的部件——磁盘。因此，我们无需过多关注那些 `*do log`，只需将其视为磁盘文件即可。

#### 掉电不丢数据是磁盘的重要特性

事实上，MySQL 完全是依靠磁盘不丢数据的特性来实现的：无论你执行了多少次 `update` 语句，无论 MySQL 有多少级缓存和多少种日志，只有当数据成功写入磁盘后，才会向客户端返回成功状态。文件的修改是有队列的，可以确保每次写操作的可靠性。

当然，这里的写入磁盘并不一定指的是真的存储到表对应的数据文件中，也可以是 `*do log`。如果服务器意外重启，MySQL 启动后会将刚才记录的这些 `*do log` 中的信息默默地写入到磁盘上真正的表数据文件中。

“数据库的单点究竟在哪里”的答案已经显而易见：数据库的单点就单在了磁盘上。
