系统设计精要 - 2
21. 事件驱动架构
事件驱动架构(Event-Driven Architecture, EDA)是一种以事件为媒介、实现服务与应用程序之间异步通信的架构范式。在同步通信模型中,调用方服务必须阻塞等待被调方返回响应后方可继续执行,这导致服务之间形成强耦合关系;一旦某一服务响应延迟或发生故障,整个调用链路的可用性都将受到连带影响。

事件驱动架构
在事件驱动架构中,服务之间不再进行直接调用。取而代之的是,服务在触发事件后立即返回,无需等待其他服务响应;其余服务通过订阅并监听该事件,进而执行相应处理逻辑。
•
服务A → 发布事件 → 向用户返回响应
•
服务B → 订阅事件并作出响应
核心构建块
•
事件(Event):对某一状态变更或行为发生的消息化描述,例如订单已创建、用户已注册、订单已送达。
•
生产者(Producer):负责发布事件的服务。
•
消费者(Consumer):负责订阅并处理事件的服务。
•
事件代理 / 消息队列(Event Broker / Message Queue):负责暂存消息的中间件,接收生产者发布的消息并将其投递给消费者。
优势
•
服务之间松耦合
•
消费者可独立横向扩展
•
异步处理
•
新增消费者成本低
22. 无服务器架构
单体架构与微服务架构均运行在由团队自行管理的服务器之上,无服务器架构则不同。该命名具有一定误导性:服务器依然存在,只是资源的配置、扩缩容、补丁更新与关停等运维工作均由云服务商代为承担。

无服务器架构
开发者只需编写函数并将其部署,当请求到达时平台会自动运行该函数,执行完毕后即释放资源。无服务器计算本质上是一种按需分配的服务器使用模式,而非真正意义上“没有服务器”。
•
事件驱动:请求到达时触发函数执行(即事件触发)。
•
自动伸缩:若并发请求数为1000,平台将启动同等数量的函数实例。
•
冷启动:由于每次处理请求均需新建运行环境,因此存在一定处理延迟,该现象称为冷启动。
•
无状态:各函数实例相互独立,不共享任何状态。
•
按使用量计费:依据实际资源消耗量进行计费。
适用场景
•
事件驱动型任务
•
流量稀疏或不可预测的场景
•
需要快速迭代开发的场景
23. 内容分发网络(CDN)
CDN(Content Delivery Network,内容分发网络)是由分布于全球各地的服务器节点构成的网络体系,通过将内容缓存并分发至更靠近用户的位置,避免请求回传至源站。
内容分发网络
工作原理
•
源站是存放所有真实内容的服务器。
•
内容被缓存至遍布全球的边缘服务器。
•
诸如 CSS、JS、HTML 与图片等静态资源会被缓存至边缘节点,并由边缘节点直接提供服务。
•
每个用户请求均被路由至最近的边缘服务器。
•
缓存命中 → 立即返回响应。
•
缓存未命中 → 从源站拉取内容,在边缘节点生成副本,并返回响应。
CDN 最适用于静态内容或更新频率较低的内容,不适用于实时性要求高、高度动态的数据,例如实时银行余额。
24. 消息队列
消息队列是位于生产者与消费者之间的缓冲层,用于暂存消息直至其被处理,从而使生产者无需等待消费者就绪。
生产者 → 发送消息 → 队列 → 消费者在就绪时取出消息
消息队列
为何不直接调用消费者?
在生产者直接调用消费者的模型中,消费者必须始终处于运行状态、响应及时且持续可用。一旦消费者宕机或处于恢复状态,调用将失败,进而导致请求失败。
队列位于生产者与消费者之间:
生产者 → 队列 → 消费者
生产者无需关心消费者是否响应缓慢或已宕机,只需将消息推送至队列即可完成自身职责。消费者可在自身节奏下获取消息并执行处理。若消费者发生崩溃,消息仍会在队列中持久化等待,不会丢失。
实际案例
•
订单服务 → 将“SendInvoice”消息推送至队列
•
订单服务 → 立即向客户端返回响应
•
发票处理程序 → 从队列拉取消息 → 生成发票
生产者立即继续后续流程,消费者按自身节奏处理。
关键特性
•
解耦:生产者与消费者之间无需相互感知。
•
缓冲:可吸收流量峰值;消费者按自身处理能力消费。
•
可靠性:消息在队列中持久化,直至被成功处理。
•
原生异步:生产者从不阻塞等待响应。
消息队列是事件驱动架构的重要组成部分。
25. 发布-订阅
消息队列采用单生产者、单消费者模型处理每条消息。而发布/订阅模式解决的是另一类问题:当多个服务均需对同一事件作出响应时,应如何处理?
发布-订阅
发布者向某一主题发布消息,所有订阅该主题的订阅者均会收到一份消息副本。与队列中每条消息仅投递给一个消费者不同,发布/订阅模式会将每条消息广播至所有订阅者。
发布者既不感知也不关心订阅者的身份,仅负责执行发布动作。
队列与发布/订阅的对比
•
队列:一条消息由一个消费者消费。
•
发布/订阅:一条消息被分发至每一个订阅者。
26. 后台处理作业
设想用户下单购买了一件商品。您正在设计一项功能,当订单提交后,系统应当:
•
向用户发送确认信息
•
生成订单发票 PDF 并发送邮件
后台任务处理
•
向其手机号码发送包含订单号与预计送达日期的短信
•
将数据同步至分析系统
若将上述所有任务均置于同一个 API 调用(即创建订单 API)中同步执行,用户将不得不等待更长时间才能收到响应。在流量高峰期,系统响应时间可能进一步延长,甚至导致请求失败。
这正是后台任务处理所解决的问题。
与其以同步方式处理所有任务,不如将非关键任务迁移至后台延后处理,优先完成主请求。主请求首先被处理,所有关键操作在此期间执行完毕,随后返回响应;非关键性操作则在稍后异步执行。
核心构建块
•
作业 / 任务(Job / Task):需要执行的工作单元,例如“发送这封邮件”“生成这份报告”。
•
作业队列(Job Queue):作业等待被空闲工作者处理的场所,如 Redis、RabbitMQ、SQS、BullMQ。
•
工作进程(Worker):独立进程,负责从队列中获取任务并执行。
后台处理作业模式
•
异步或基于事件
•
计划任务或 Cron 作业
•
重复执行的作业
•
延迟作业
27. 定时任务
定时任务是指在固定时间点或按重复计划自动运行的任务,无需任何用户或请求触发。
Cron作业
•
常规请求:客户端 → 发送请求 → 服务器响应
•
Cron 作业:时钟到达预定时间 → 作业被触发
Cron 作业通过特定语法进行调度:
代码块
Plain Text 自动换行复制
[ * * * * * ]
•
0 0 * * * → 每天午夜执行
•
0 */6 * * * → 每6小时执行一次
使用场景
•
每日数据库备份
•
每日报表生成
•
周期性数据清理
•
每6小时发送邮件告警
28. 反向代理
反向代理部署于服务器前端,用于将服务器对客户端隐藏。客户端不再直接与真实服务器通信,而是先与反向代理建立连接。反向代理在内部将请求转发至相应服务器,获取响应后再返回给客户端。从客户端视角来看,代理即服务器,客户端无法感知其背后的真实拓扑。
反向代理
•
无反向代理时:客户端 → 服务器(客户端直接访问服务器)
•
有反向代理时:客户端 → 反向代理 → 服务器(客户端仅感知代理)
使用场景
•
负载均衡、SSL 终止、缓存、安全防护、请求路由、响应压缩
29. 正向代理
正向代理部署于客户端与互联网之间,代表客户端发起请求。客户端不再直接与服务器通信,而是先与代理服务器交互。代理服务器代为转发请求并接收响应。从服务器视角来看,请求来源于代理而非客户端,服务器无法获知真实客户端身份。
正向代理
使用场景
•
隐私保护、内容过滤、缓存、绕过地理限制、日志记录与监控
正向代理 → 位于客户端之前,使服务器无法直接感知客户端。
30. 身份验证
身份验证是指核实用户身份的过程。以一个员工管理应用程序为例,任何用户若要使用该应用,均须输入用户名与密码。
身份验证
用户 → 用户名 + 密码 → 服务器验证凭据 → 身份确认 → 用户登录
此时服务器已确认用户身份,这一过程即身份验证。
常用身份验证方式
•
用户名 + 密码、一次性密码(OTP)、生物识别、API 密钥、OAuth、JWT、基于会话的身份验证
31. 授权
授权
授权是指验证用户是否具备执行某项操作的足够权限的过程。用户可能已通过身份认证,但尚未被授予对特定资源或操作的访问权限。
例如,一个员工管理应用程序包含两类用户:
管理员
•
添加员工
•
删除员工
•
更新员工信息
•
查看薪资信息
员工
•
查看个人档案
•
更新个人资料
•
查看已分配任务
设想一名员工试图删除另一名员工:该用户已通过身份验证,但未被授权执行该操作。
身份验证与授权的区别
•
身份验证 = 你是谁?
•
授权 = 你被允许做什么?
32. JWT - JSON Web Token
JWT(JSON Web Token)是一种用于身份验证与授权的令牌机制。它使服务器无需查询数据库即可验证用户身份,因为令牌本身携带了数据,并附带签名以证明其未被篡改。
JSON Web 令牌
JWT 的组成部分
标头(Header)
包含用于对令牌进行签名的算法,例如 HS256 或 RS256。
载荷(Payload)
包含实际数据,称为声明(Claims),例如 userId、角色与过期时间。
签名(Signature)
通过使用密钥对头部与载荷进行签名生成,用于证明令牌未被篡改。
工作原理
登录 → 服务器生成 JWT 并使用密钥签名,随后发送给客户端;客户端存储该令牌,并在每次请求的 Authorization 头中携带;服务器验证签名,若签名有效,则信任令牌中的数据,无需查询数据库。
33. OAuth
OAuth 是一种允许用户使用现有账号(如 Google 或 GitHub)进行登录或授权访问的机制,无需向应用提供密码。
它解决的问题
在没有 OAuth 的情况下,若希望用户通过 Google 登录,则必须让 Google 以某种方式将用户密码共享给应用。这显然是不可接受的,因为应用将因此具备直接登录用户真实 Google 账户的能力。
OAuth 完全规避了这一问题:应用永远不会接触到用户密码。
OAuth 的本质
OAuth 是一种授权框架,用户可在不共享凭据的前提下,授予应用对其在其他服务上数据的有限访问权限。应用将获得一个令牌,其权限范围仅限于应用所需内容。
OAuth 流程
用户点击“使用 Google 登录”
应用将用户重定向至 Google 登录页
用户直接在 Google 页面登录,密码仅输入至 Google,而非应用
Google 请求用户授权,例如“此应用需要您的电子邮件和个人资料”
用户同意授权
Google 重定向回应用,并附带授权码
应用使用授权码换取访问令牌
应用使用访问令牌从 Google 获取用户信息
34. Cookie、本地存储与会话存储
客户端存储允许浏览器保存少量数据,从而使应用无需每次均从服务器获取全部内容。以下是三种常见方案。
Local Storage vs Session Storage vs Cookies
Cookie
小块数据,会在每次向服务器发送 HTTP 请求时自动随请求携带。
本地存储(Local Storage)
在浏览器中存储数据,即使关闭并重新打开浏览器后仍会保留。
会话存储(Session Storage)
与本地存储机制相同,但数据仅在该浏览器标签页的会话期间有效。
35. 负载均衡
设想您正在构建一个电子商务应用,并计划在午夜举行大型促销活动。突然间,成千上万的用户开始访问应用,单台服务器收到的请求数量超出其处理能力。此时应如何应对不断增长的流量?
答案是增加更多服务器。
但随之而来的新问题是:如何决定将哪个请求发送至哪台服务器?
这正是负载均衡所解决的问题。
负载均衡是指将传入请求分发至多台服务器的过程。负载均衡器部署于服务器前端,负责决定每个传入请求应被转发至哪台服务器,从而避免某台服务器因请求过多而过载,同时确保其他服务器不会处于空闲状态。请求的转发目标取决于所采用的负载均衡算法。
36. 负载均衡算法 - 轮询
负载均衡算法 - 轮询
请求按顺序在各服务器间依次分配。当分配至最后一台服务器后,循环重新开始。
•
请求1 → 服务器A
•
请求2 → 服务器B
•
请求3 → 服务器C
•
请求4 → 服务器D
•
请求5 → 服务器A
•
请求6 → 服务器B
37. 负载均衡算法 - 最少响应时间
负载均衡算法 - 最小响应时间
请求被发送至活动连接数最少的服务器。
•
服务器A → 10个连接
•
服务器B → 3个连接 ← 下一个请求
•
服务器C → 9个连接
该算法可能优于轮询法,因为并非所有请求的完成时间均相同。
38. 负载均衡算法 - 最少连接数
负载均衡算法——最少连接
请求根据服务器性能进行路由。负载均衡器会综合考虑以下因素:
•
当前活动连接数
•
最近的响应时间
请求被发送至响应更快、负载更轻的服务器。
39. 负载均衡算法 - 加权轮询
负载均衡算法 - 加权轮询
部分服务器可能具备更强的处理能力。权重机制使性能更强的服务器能够接收更多流量。
•
服务器A → 权重3
•
服务器B → 权重1
每个周期内:
•
服务器A → 3个请求
•
服务器B → 1个请求
40. 负载均衡算法 - IP 哈希
负载均衡算法 - IP哈希
客户端的 IP 地址用于确定由哪台服务器接收请求。
客户端IP →(哈希函数)→ 服务器B
同一客户端通常会被路由至同一台服务器,这有助于实现会话持久化(粘性会话)。