系统设计精要 - 1
客户端-服务器架构

客户端-服务器架构
客户端-服务器架构几乎是所有系统的基石。
✅
客户端发送请求,服务器在与数据库交互的同时处理这些请求。当客户端直接连接到数据库时,这种架构被称为两层架构;一旦在客户端与数据库之间加入专门的应用服务器,就形成了三层架构——这也是当今大多数现代应用所采用的部署模式。诸如缓存和扩展等概念都建立在这一基本的客户端-服务器关系之上。
垂直扩展与水平扩展
扩展系统是指在不导致系统崩溃的前提下,提升系统处理更大负载的能力。系统可能面临的负载形式包括更多的请求、更频繁的数据库访问等。扩展系统主要有两种方式

垂直扩展与水平扩展
垂直扩展
在这种类型中,我们会在现有系统上增加更多性能或提升规格。这意味着
•
增加更多内存、增加更多计算能力、增加更多存储
在初期阶段,垂直扩展非常有效,但很快就会遇到瓶颈。由于硬件限制,系统无法无限扩展。
水平扩展
在这种模式下,我们通过增加更多机器来扩展,而不是对现有机器进行升级。这也被称为扇出模式。多台机器协同工作以完成一项任务。
数据库
每个应用程序都需要一个用于存储数据的场所。对于电子商务应用,我们需要一种存储方案来保存用户、商品、订单和消息的相关信息;而对于聊天应用,则需要存储用户、群组及其消息。这正是数据库的用途。

数据库是一组经过组织的数据集合,即使在服务器重启或发生故障后,也能可靠地存储、检索、更新和删除数据。服务器并不存储数据,它仅负责从数据库中读取、更新或删除数据。
数据库支持四种操作类型:创建、读取、更新、删除。也称为 CRUD 操作。
数据库的类型
•
关系型数据库 、非关系型数据库
•
图数据库 、面向对象数据库
•
层次型数据库 、向量数据库
ACID 与 BASE
数据库采用不同的模型来管理事务、一致性、可用性和可扩展性。ACID 和 BASE 是数据库系统中常用的两种方法,各自针对不同的应用需求而设计。

ACID 特性与 BASE 特性
ACID(SQL数据库)
ACID代表原子性、一致性、隔离性和容错性。
•
原子性 - 一个事务要么完全执行成功,要么完全不执行,不存在部分更新的情况。
•
一致性 - 事务将数据库从一个有效状态转换为另一个有效状态,且始终不违反已定义的规则。
•
隔离性 - 同时运行的事务彼此互不干扰。
•
持久性 - 一旦事务提交,即使系统随后立即崩溃,其结果也会被永久保存。
BASE特性(NoSQL数据库)
BASE代表基本可用、柔性状态和最终一致性。BASE特性将可用性置于一致性之上,并允许出现暂时的不一致数据。
•
基本可用 - 系统在任何时刻都可进行读写操作
•
柔性状态 - 由于同步操作,状态会在无任何输入的情况下发生变化
•
最终一致性 - 数据会随着时间逐渐一致,而非瞬间一致。
CAP定理
CAP定理
CAP定理指出,在一个分布式系统中,你只能同时保证以下三项中的两项。
•
一致性:无论由哪个节点处理,每一次读取都能获得最新的写入数据。
•
可用性:每个请求都会得到响应,即使部分节点出现故障。
•
分区容错性:即使节点之间的网络通信中断,系统仍能继续运行。
为什么不能同时满足三者?
网络分区迟早会发生。一旦节点之间无法通信,你就不得不做出选择。
•
CP系统 - 在分区期间选择一致性而非可用性。ACID 倾向于 CP。严格保证正确性,为保持一致性而愿意拒绝请求。
•
AP系统 - 在分区期间选择可用性而非一致性。BASE 倾向于 AP。保持可用性,接受最终一致性。
•
CA 系统 - 选择一致性与可用性,放弃对分区的容忍。
关系型数据库
关系型数据库
✅
关系型数据库,也称为SQL数据库,是一种以行和列的形式存储数据、并按表进行组织的数据库类型。其数据遵循严格且预先定义的模式,因此表中的每一行都具有相同的结构。表之间通过外键相互关联,外键用于表示不同实体之间的关系,例如用户与其订单之间的关系。
这种结构使您能够通过联接执行复杂查询,在单个查询中整合多个表中的数据。SQL 数据库还遵循 ACID 特性,因此在对准确性要求较高的场景中(如银行业务和订单管理)尤为可靠。
关系型数据库的常见示例:PostgreSQL、MySQL 和 Oracle。7. 非关系型数据库
非关系型数据库
非关系型数据库
✅
非关系型数据库,也称为NoSQL数据库,是一种数据无需遵循固定表结构的数据库类型。相反,数据以灵活的格式存储,例如文档、键值对或宽列,具体取决于数据库的类型。
每条记录都可以具有不同的结构,因此您无需在一开始就受限于严格的模式。这种灵活性使NoSQL数据库非常适用于非结构化或快速变化的数据,以及需要在多台服务器上实现横向扩展的应用程序。大多数NoSQL数据库更倾向于可用性和速度,而非严格的一致性,遵循BASE而非ACID。
常见的示例包括 MongoDB、Redis 和 Cassandra。
数据库复制
数据库复制
创建并维护同一数据库的多个副本,且各副本之间保持同步,这一过程称为数据库复制。
复制的类型
•
同步复制:主节点会等待从节点确认写入命令后,再向服务器(对于数据库而言即客户端)返回响应。
客户端 -> 写入 -> 主节点 -> 向从节点写入 -> 向客户端确认
•
异步复制:主节点会立即响应客户端,并在后台向副本发送写入信号。
客户端 -> 写入 -> 主副本 -> 向客户端响应 -> 在后台将数据写入副本。
数据库分区
数据分区
数据分区是指将主数据库拆分为多个独立的小型数据库,以实现更高效的处理与存储。数据库分区主要分为两种类型。
垂直拆分:在这种方式下,我们按列对表进行拆分。同一行中的不同列分别存储在不同的表或存储中。
水平分区:在这种类型中,我们 按行拆分表。列相同,但不同的行位于不同的分区中。
数据库分区的优势:
•
更小的片段易于管理和扩展
•
查询简便快捷
•
不再需要的数据可以移至单独的数据库并归档。
数据库分片
数据库分片
✅
数据库分片是一种通过将数据库拆分为多个独立的片段(称为分片)来实现扩展的方法,每个分片仅存储总数据的一部分。
这样一来,不再由单个数据库处理所有的读写操作,而是将负载分散到多个分片上,每个分片都在各自的服务器上运行。数据库分片是一种水平式的数据库分区方式。
分片键决定了某条数据属于哪个分片,通常采用基于范围、基于哈希或基于地理位置的策略。当单个数据库无法再承受写入负载或总数据量时,这种方法尤为有用;而仅靠复制则无法解决这一问题,因为每个副本仍然存储着完整的数据集。权衡在于,跨多个分片的查询会变得更加困难,而且跨分片事务也难以保持原子性。
HTTP
HTTP - 超文本传输协议
HTTP是超文本传输协议的缩写,是一种用于客户端与服务器之间进行通信和数据传输的协议。HTTP包含一套规则,规定了通信的建立方式、请求的格式与发送方式,以及响应的返回方式。HTTP是一种无状态协议,底层使用TCP。
每个HTTP请求都包含:
方法——要执行的操作、URL - 资源的地址、Headers - 请求元数据、正文 - 客户端发送给服务器的数据
每个HTTP响应都包含:
状态码 - 请求的结果、头部 - 响应的元数据、正文 - 请求的数据
HTTP方法:
GET - 获取数据、POST - 创建新数据、PUT - 更新/替换数据、PATCH - 原地更新数据、DELETE - 删除数据
HTTP 状态码
200 - 确定、201 - 已创建、301 - 已移动、304 - 未修改、400 - 错误请求、401 - 未授权、404 - 未找到、500 - 内部服务器错误、503 - Service Unavailable
单体架构
单体架构
单体架构是一个单一的统一代码库,其中应用程序的所有功能都集中在一起,并作为一个整体运行。
一个代码库 - 一次构建 - 一次部署
单体架构进一步细分为两种类型
•
传统单体架构:指所有功能模块,包括用户界面、业务逻辑层、数据访问层以及数据库,都部署在同一代码仓库中的应用。例如:Java 或 Spring 应用程序
•
模块化单体/现代单体:一种现代的单体架构形式,其中仅服务层保持单体化,而用户界面和数据库则被独立分离。
单体架构的优势:
•
易于搭建、开发、测试与部署
•
执行速度快
•
非常适合小型团队且项目范围有限
单体架构的缺点:
•
应用规模扩大后难以维护
•
各层之间耦合紧密
•
多团队难以独立协作
•
技术栈锁定
微服务架构
微服务架构
在微服务架构中,系统不再构建为一个庞大的单体应用,而是被拆分为多个小型、独立的服务。每个服务只负责一项业务功能,拥有自己的代码库、部署流水线,并且通常还维护着自己的数据库。这种架构允许应用程序被设计为一组松散耦合、相互依赖的服务,这些服务可以独立开发、部署和维护。
在使用微服务构建的电子商务应用中:
用户服务负责身份认证与用户资料
产品服务负责商品目录与库存管理
订单服务负责订单创建与跟踪
支付服务负责交易处理
微服务的优势:
•
独立部署性、可扩展性、故障隔离与关注点分离、技术灵活性、更短的开发周期
微服务的劣势:
•
复杂通信、数据管理、运营开销、测试复杂性、延迟
API网关
API网关
API 网关位于客户端与服务器之间,仅向客户端暴露所需的部分,隐藏所有复杂性。API 网关是应用程序所有客户端的单一入口点,介于客户端与微服务之间。
API网关的职责包括:
•
处理来自客户端的每一条请求的身份验证与授权、将请求路由至正确的微服务
•
日志与监控、负载均衡、限流、缓存
•
更改协议(请求可以是HTTP,已改为gRPC用于服务间通信)。
亚马逊的API网关是最常见的API网关服务之一。其他服务还包括NGINX和Kong。
REST API
REST API
✅
REST,即表述性状态转移,是在 HTTP 基础之上构建的一套用于设计 API(应用程序编程接口)的规则。
REST API 就像一套规则与契约,使客户端与服务器能够以可预期的方式进行交互。
REST遵循六项原则
统一接口:在整个 API 中,采用一致的命名、标准的接口、统一的结构,并始终如一地使用 HTTP 方法和状态码。
无状态:不保存任何请求状态,每个请求都必须包含服务器所需的所有信息。
客户端-服务器:客户端与服务器之间实现接口与关注点的分离。
可缓存:响应可以被显式标记为可缓存或不可缓存,并且可以复用先前获取的响应
分层系统:在基础架构(客户端-服务器)之间可以引入多层。客户端只能看到与其直接交互的那层,而无需关心该请求是否最终被转发至目标服务器。
按需代码:服务器可以通过向客户端发送可执行代码来扩展或增强客户端的功能。
SOAP API
✅
SOAP,即简单对象访问协议,是一种用于在系统之间交换结构化信息的简单协议。它对消息格式有严格的规定,且通信仅以XML方式进行。
与REST不同,REST是一种API风格,使用JSON、XML以及纯文本进行通信。SOAP要严格得多,也更规范。
每个 SOAP 请求/响应都封装在名为“Envelope”的 XML 结构中。Envelope 的头部包含元数据,而主体则包含实际的请求/响应数据以及错误详情。
示例 SOAP 请求:
代码块
XML
GraphQL
GraphQL
REST API 存在两个问题
•
过度获取:获取的数据多于所需。
•
获取不足:由于单次API响应不够,需要进行多次API调用。
GraphQL 同时解决了这两个问题。GraphQL 是一种用于 API 的查询语言,客户端只需在一次请求中明确指定所需的数据,不多不少。与 REST 不同(REST 采用多个端点、固定响应),GraphQL 只暴露一个端点,由客户端自行定义返回的数据结构。 POST /graphql.就这么简单。一个端点搞定所有操作。
示例:获取个人主页
REST 等价方案(需两次调用):
代码块
Plain Text
GET /users/12
GET /users/12/orders?limit=3
每次调用都会返回其自身的完整对象——你会得到一些不需要的字段(如电子邮件、地址),并且仍然需要在客户端将两个响应拼接在一起。
GraphQL(1次调用):
请求(客户端所请求的内容):
代码块
JavaScript
{
user(id: 12) {
name
profilePicture
orders(limit: 3) {
id
total
}
}
}
响应(完全符合所请求的形状):
代码块
Plain Text
{
"user": {
"name": "Anurag",
"profilePicture": "url...",
一次请求。精确指定字段。既不过量获取,也不不足获取,无需拼接响应。
WebSocket
Websocket
WebSocket 是客户端与服务器之间的一种持久的、双向连接。一旦连接建立,客户端和服务器即可相互发送消息,而无需反复建立连接或进行握手。
WebSocket 连接最初以普通的 HTTP 请求发起,但当服务器返回 101 状态码时,连接即升级为 WebSocket 连接。收到 101 状态码后,请求-响应周期结束,连接随即成为一条开放的、双向通信通道。
现实中的案例:
•
聊天应用、在线游戏、体育或股票类应用、外卖应用、像Google Docs这样的协作工具
gRPC
gRPC
gRPC,即Google远程过程调用,是一种高性能框架,它使一台服务器能够像调用本地函数一样,调用运行在完全不同的另一台机器上的另一台服务器上的方法。
例如,在 OrderService 内部,您可以这样做
代码块
JavaScript
这感觉就像调用同一文件中的一个函数,但其底层却涉及一次网络调用,目标是运行在完全不同的另一台机器上的服务器。
gRPC为何如此高效?
•
采用 ProtoBuf,gRPC 将数据序列化为紧凑的二进制格式。有效载荷更小,解析速度更快。
•
使用 HTTP/2,其速度优于 HTTP/1.1(REST 所采用的协议)
何时使用gRPC?
•
在对速度要求较高的服务器间通信场景中
•
对低延迟有要求的系统、具备流式传输的系统
轮询
Web 轮询
设想您正在构建一个电子商务应用的结算页面。在该页面上,用户需要完成支付,他们可以使用手机扫描二维码进行付款。在此场景下,客户端应在无需用户手动刷新的情况下自动更新支付状态。我们需要服务器在支付成功后自动通知客户端,而无需客户端主动查询。如果没有 Webhook,我们的应用将不得不每秒向 PaymentService 查询一次支付状态。
代码块
Plain Text
Client -> GET /payments/33/status
Client -> GET /payments/33/status
Client -> GET /payments/33/status
Client -> GET /payments/33/status
这称为轮询。客户端会反复向服务器询问是否有任何变化,并根据服务器的响应作出相应处理。
适用场景:
支付网关
实时仪表板
聊天应用
缺点
•
效率低下且浪费资源
•
由于持续的请求,服务器可能会过载
Webhook
WebHooks
✅
Webhook 是 Web 轮询的更优替代方案。Webhook 颠倒了通信方向:不再由客户端先向服务器发起请求,而是在事件发生时由服务器主动调用客户端。
客户端 → “这是我的URL,付款完成后请给我打电话” → 服务器保存此URL
服务器 → POST https: → 客户端。
客户端从不主动询问发生了什么,而是在事件发生后由服务器主动推送更新。REST、GraphQL 和 gRPC 都是基于拉取的机制,即客户端主动获取信息;而 Webhook 则是基于推送的机制,即服务器将事件推送给客户端。