数据库
数据库如何存储、组织并可靠地检索应用程序所依赖的持久化数据。
数据库为何存在
应用内存是临时的。真实系统需要能够在崩溃、重启和部署后仍然保留的持久化存储。
- 服务器内存(RAM)是易失性的,进程停止时会被清空。
- 关键数据——用户、订单、消息、日志——必须在执行结束后仍然保留。
- 数据库提供由磁盘支持的持久化、结构化存储。
详情
后端服务器本质上只是一个受操作系统控制的运行中进程。它的内存变量会在进程重启、崩溃或重新部署时消失。这使得内存非常适合计算,但完全不适合长期存储。
生产系统需要数据持久性。用户账户在重启后仍然必须存在。订单在部署后仍然必须被记录。日志必须保持可访问,以便调试和审计。
数据库通过将结构化数据写入持久化存储(通常是 SSD 或磁盘)来解决这个问题。内存针对速度进行了优化,而存储针对持久性进行了优化。数据库引擎负责管理数据如何被写入、索引以及安全恢复。
没有数据库,你的系统就没有长期记忆。它会以最糟糕的方式变成无状态——无法在故障中存活。
什么是数据库?
数据库是一种受管理的系统,用于持久存储数据、按结构组织数据,并支持受控检索。
- 将数据可靠地持久化到磁盘上,且在进程生命周期结束后仍然保留。
- 将数据组织为结构化模型(表、文档、键值)。
- 提供查询引擎,并通过约束来保证完整性。
详情
数据库不仅仅是存储;它还是一个用于数据的受控执行引擎。
在关系模型中,数据存储在由行和列组成的表中。每一行表示一条记录,每一列表示一个属性。模式定义了结构、数据类型和约束,防止插入无效或不一致的数据。
数据库还提供查询引擎。应用程序不会直接读取原始文件,而是发出结构化查询,以高效地检索或修改特定的数据子集。
从系统层面来看,数据库是权威的事实来源。它确保持久性,维护结构完整性,并支持可预测的状态管理。
SQL 与 NoSQL
SQL 和 NoSQL 数据库以不同的结构和扩展权衡解决类似的存储问题——没有哪一种是普遍更优的。
- SQL: 结构化 schema、基于表的模型、强一致性保证。
- NoSQL: 灵活的 schema,通常基于文档或键值对,专为水平扩展而设计。
详情
SQL 数据库(关系型系统)将数据组织到具有严格 schema 的预定义表中。表之间的关系会被显式建模,并且强事务保证是标准特性。这使它们非常适合需要完整性和复杂查询的系统。
NoSQL 数据库放宽了严格的 schema 约束。许多 NoSQL 数据库使用基于文档或键值对的结构,允许不同记录之间的字段不一致。这种灵活性可以简化快速演进的数据模型的开发,并且更自然地支持跨分布式集群的水平扩展。
真正的区别在于架构侧重点。SQL 优先考虑结构和强一致性。NoSQL 优先考虑灵活性和分布式可扩展性。生产系统通常会根据工作负载同时结合两者。
当服务器查询数据库时会发生什么?
在大多数真实系统中,决定请求延迟的主要不是应用逻辑,而是数据库调用。
- 请求流会延长:客户端 → 负载均衡器 → 服务器 → 数据库 → 服务器 → 客户端。
- 服务器在数据库处理查询时会阻塞(或等待)。
- 数据库往返通常决定整体响应时间。
详情
当客户端发送一个 HTTP 请求时,它最终会到达你的服务器进程。你的处理程序会执行应用逻辑——但大多数有意义的操作都需要读取或写入持久化数据。
服务器会向数据库引擎发送查询。这个查询可能涉及解析 SQL、检查权限、定位相关数据页、使用索引、从磁盘读取数据,以及组装结果集。
在这段时间里,服务器线程通常处于等待状态。在同步模型中,它会阻塞。在异步模型中,事件循环会等待结果。无论哪种方式,进展都取决于数据库完成它的工作。
这一点非常关键:数据库交互通常是后端系统中延迟的最大来源。糟糕的查询、缺失的索引、网络往返,或磁盘 I/O 延迟,都很容易超过应用计算时间。
如果你的系统感觉很慢,数据库层通常是首先要排查的地方。
索引
索引帮助数据库快速找到数据,而不需要检查每一行。
- 没有索引 → 数据库逐行检查。
- 有索引 → 数据库直接跳到匹配的数据。
- 索引可以加快读取,但在写入数据时会增加额外工作。
详情
假设你有一张包含一百万用户的表,并且你要搜索某个特定的电子邮件地址。
如果没有索引,数据库可能需要查看每一行,直到找到匹配项。随着表变大,这会很慢。
如果在 email 列上有索引,数据库会维护一个单独的、有组织的结构(类似书籍的索引)。它不会扫描全部数据,而是使用这个结构快速定位正确的行。
索引可以让读取快很多,但它们不是免费的。每次你插入、更新或删除数据时,索引也必须同步更新。
如果某个查询意外地很慢,首先要问的问题之一是:“被搜索的列上有没有索引?”
事务与一致性
事务确保一组相关操作要么一起全部成功,要么一起全部失败。
步骤按顺序执行。如果其中一步失败,之前的所有更改都会被撤销,因此数据库不会最终处于半更新状态。
- 多个数据库操作可以组合成一个逻辑单元。
- 如果某一步失败,之前的所有更改都会回滚。
- 这可以防止数据最终处于部分更新的状态。
详情
考虑把钱从账户 A 转到账户 B。
步骤 1:从账户 A 扣除 $100。
步骤 2:向账户 B 加入 $100。
如果系统在步骤 1 之后、步骤 2 之前崩溃,钱就会消失。这就是数据损坏。
事务可以防止这种情况。两个操作都被包裹在一个事务边界内。数据库保证以下两种情况之一成立:
• 两个步骤都成功完成,或者
• 两个步骤都不会被永久应用。
这种“要么全部发生,要么全部不发生”的行为称为原子性。它确保系统即使在发生故障时也能保持逻辑一致。
只要数据正确性很重要,事务就必不可少——金融、库存、身份验证,以及更多场景。
ACID
ACID 定义了确保数据库事务正确且具备抗崩溃能力的四项保证。
- 原子性:一个事务要么完全成功,要么完全回滚。
- 一致性:已提交的事务之后,数据仍保持有效。
- 隔离性:并发事务不会相互破坏。
- 持久性:已提交的数据在崩溃和重启后仍然存在。
详情
ACID 是一种实用的可靠性契约,而不是理论。
原子性可防止部分更新。如果某一步失败,所有操作都会回滚。
一致性确保事务完成后,约束、关系和规则都得到保留。
隔离性在多个用户同时操作时,保护事务彼此不被破坏。
持久性保证一旦数据库确认成功,数据就会写入持久存储,并且能够在系统故障后继续存在。
总的来说,这些特性让数据库成为真实世界系统中可靠的基础。
数据库扩展
随着流量和数据增长,单个数据库实例往往会成为瓶颈。扩展策略可以分散负载或提升容量。
- 垂直扩展会提升单个数据库机器的性能。
- 读副本将读取流量分散到数据的多个副本上。
- 分片将数据拆分到多个数据库中,以分散整体负载。
详情
随着使用量增长,查询量也会增加。最终,单个数据库服务器无法跟上传入请求的速度。
一种方法是垂直扩展——通过增加更多 CPU、内存或更快的存储来升级现有机器。这是最简单的解决方案,因为架构保持不变。不过,硬件升级有上限,而且成本会迅速上升。
另一种策略是添加读副本。在这种模式下,主数据库负责写入,而复制出来的副本负责只读查询。这可以减轻主服务器的压力,不过由于数据在节点之间传播,可能会引入轻微延迟。
对于更大规模的增长,系统可能会使用分片。不是把所有数据都存放在一台机器上,而是将数据集分布到多个数据库实例中。例如,一个分片可能存储 A–M 的用户,另一个存储 N–Z 的用户。这会提高总容量,但需要仔细的路由逻辑和运维管理。
这些方法都能提升可扩展性,但它们也会在成本、复杂性和一致性方面带来权衡。
数据库故障与瓶颈
当数据库变慢或发生故障时,整个应用会立即受到影响。
应用程序
等待数据库的请求
数据库状态
- 如果数据库不可用,请求会立即开始失败。
- 慢查询或锁竞争会导致整个应用的延迟上升。
- 资源耗尽——连接、磁盘或复制延迟——会导致系统不稳定。
详情
数据库通常是后端系统中最关键的依赖。当它变慢或发生故障时,应用会立即反映出影响。
如果数据库无法访问,请求处理程序就无法完成查询,应用会开始返回 500 级错误。
即使数据库正在运行,慢查询、缺失索引或锁竞争也会增加延迟。从外部看,服务器似乎很慢,但实际上它是在等待数据库响应。
在高负载下,连接耗尽、复制延迟或磁盘饱和等资源限制会引入不稳定和不一致的行为。
许多“应用问题”实际上源自持久化层。理解这条依赖链对于诊断生产环境问题至关重要。
问题部分
1 / 5