速成课 · No. 03
架构几乎从来不是凭空发明的。你要做的是识别:哪一种已知的形状契合眼前的问题——以及这个形状要收取什么代价。
17 个模式 · 每个一张图 · 末尾附速查表
模式是对一个在成千上万个系统里反复出现的问题的、经过验证的答案。每个模式都有名字——而且几乎总是英文名,人们在文档里搜的、面试里聊的,都是这个名字。
模式是一个答案的名字,不是一条规则
下棋时你不会每盘都重新发明开局。你认出"这是西西里防御"——于是已经知道它大致会通往哪里。
架构模式(architectural pattern) = 反复出现的问题 + 经过验证的解决形状 + 已知的代价。当你说"服务需要对别处发生的事情做出反应",这已经是一个被解决、被命名的问题——Event-driven architecture 模式。懂这套目录,意味着你不再重新发明,而是开始做选择。
架构模式 ≠ 设计模式
设计模式是你怎么摆放一个房间里的家具。架构模式是整栋房子的平面图。
设计模式(design patterns)——Factory、Observer、Strategy——活在一个模块的代码内部。架构模式决定系统里有哪些大"盒子"、它们之间有哪些连线:什么被单独部署、谁和谁对话、数据住在哪里。这门课讲的是平面图,不是家具。
地图:六组 17 个模式
地铁线路图:先看清整条线路,再一站一站地坐。
这门课的所有模式分成六组。每组回答一个问题:怎么组织单个应用、依赖指向哪里、怎么切分系统、各部分怎么通信、怎么在数据里保存真相,以及罕见问题需要哪些形状。
怎么读每个模式
护照:每本的栏目都一样,所以很容易比较。
接下来每个模式都是同样的结构:英文名(记住它——这就是术语)、一个生活中的画面、一张图,以及一张卡片,包含这些栏目:问题(哪里痛)、思路(怎么解决)、例子、代价(你要付出什么)、何时使用,以及和其他模式的关联。最重要的规则:不存在最好的模式,只有合适的模式,而每个模式都有一张账单,要在签字之前读明白。
你不该挑那个出名的模式。你该挑那个你付得起代价的模式。
大多数系统起步时都是单个可部署的应用。最初三个模式讲的是怎么安排它的内部,让它不至于变成一团烂泥。
Layered Architecture —— 分层架构
一家餐厅:大堂接单,厨房做菜,储藏室存货。服务员不下厨,厨师不跑地窖——每个人只和下面那一层打交道。
最常见的模式。代码被切成一层层横向的层(layers),请求从上往下穿过它们。每一层只知道它下面那一层。
- 问题
- 所有代码堆成一团:改界面会弄坏数据库访问。
- 思路
- 把代码按楼层摆放:表现层、业务逻辑、数据访问、数据库。
- 例子
Web 应用:
Controller调Service,Service调Repository,Repository打到 Postgres。- 代价
- 所有东西作为一个整体部署;一个小改动 = 重新部署整个应用。
- 何时使用
- 几乎总是可以作为单个应用的起步形状。
- 关联
- 是 Modular Monolith 的基础;层内部常常会用到 Hexagonal。
Modular Monolith —— 模块化单体
一栋公寓楼:一个屋顶、一个大门,但每套公寓有自己的墙和门。没人穿过邻居的卧室去走楼梯。
仍然是一个应用,但内部按业务含义切成一个个模块(modules)——计费、商品目录、账户。模块之间只通过公开接口对话,不碰别人的表。
- 问题
- 单体变成"一大团泥巴":什么都依赖什么。
- 思路
- 按业务模块切分,接口清晰,但作为一个整体部署。
- 例子
catalog模块只调用billing的公开函数,而不是它的表。- 代价
- 需要守住边界的纪律(如果不用工具检查,边界很容易被打破)。
- 何时使用
- 几乎对所有人都是正确的默认选项——有微服务的清晰,没有网络的麻烦。
- 关联
- 自然地长成 Microservices:需要时把某个模块抽出去变成服务。
MVC / MVVM —— 让屏幕离脑子远一点
一座剧场:剧本、演员和导演、舞台和灯光,是不同的活儿。换布景不用重写整出戏。
这些模式在界面和数据与规则之间立起一道墙,这样改变某样东西长什么样,不会弄坏它做什么。
- 问题
- 逻辑散落在各个界面里;改设计 = 重写规则。
- 思路
Model 是数据和规则,View 是显示,Controller(或 ViewModel)是两者之间的黏合剂。
- 例子
Django 和 Rails 基于 MVC;移动应用和富 web 应用基于 MVVM,由 ViewModel 持有屏幕状态。
- 代价
- 界面一大,"黏合剂"就会膨胀;要留意别让逻辑漏进 View。
- 何时使用
- 任何有界面的应用。
- 关联
- 这就是用在 UI 上的 Layered 分层。
单体不等于一团乱。没有内部隔墙的单体,才是。
两个模式,同一个想法:业务规则在中心,对 framework、数据库和 web 一无所知。所有技术性的东西都在外面,接到核心上。
Hexagonal Architecture —— 端口与适配器
一台游戏机有标准接口。游戏不在乎你插的是手柄、方向盘还是街机摇杆——它只读"输入"。
装着业务逻辑的核心声明一组端口(ports)——像"保存订单"或"扣款"这样的接口。外面的**适配器(adapters)**针对具体技术实现这些端口。Driving adapters 调用核心(HTTP API、CLI、测试),driven adapters 被核心调用(数据库、支付、邮件)。
- 问题
- 业务逻辑长进了 framework 和数据库里——没法测试,也没法改。
- 思路
- 核心只和端口(接口)对话;技术住在适配器里。
- 例子
- 端口
OrderRepository:生产环境用 Postgres 适配器,测试里用一个内存列表。 - 代价
- 更多接口和文件;对一个小小的 CRUD 来说是多余的。
- 何时使用
- 有真正的业务规则,而且它们要比所用的技术活得更久。
- 关联
- Clean / Onion 是同一个想法,只是画成了圆圈。
Clean / Onion Architecture —— 依赖一律指向内
一颗洋葱:心在正中,外面裹着一层层。剥掉外皮不会伤到心——而心并不依赖外皮。
同样的原则,画成一圈圈同心圆。中心是实体(entities),外面一圈是用例(use cases),再外面是适配器,最外层是 framework 和数据库。核心规则是 dependency rule:每一支依赖箭头都指向中心。
- 问题
- 换 framework 或数据库,就得重写业务逻辑。
- 思路
- Dependency rule:内圈不从外圈 import 任何东西。
- 例子
"金额超过 1 万的发票需要审批"这条规则,是一个普通的 class,不 import web framework 或 ORM。
- 代价
- 更多的层,以及层与层之间的数据映射。
- 何时使用
- 规则复杂、生命周期很长的产品。
- 关联
- 是 Hexagonal 的双胞胎;常常住在 Modular Monolith 的模块内部。
Framework 是细节。把它当墙纸看待,而不是地基。
到了某种规模——负载的,或者团队数量的——一个应用就撑不住了。这些模式把系统切成独立的部分,作为交换,塞给你一张网络,连同它所有的故障。
Microservices —— 微服务
一座美食广场:每个摊位有自己的厨房、菜单和收银台。披萨摊忙起来就多加两个厨师;沙拉摊不需要。
系统被拆成一个个小服务(services):每个拥有一项业务能力和自己的数据库,单独部署、单独伸缩。服务之间通过网络对话——HTTP 或消息总线。
- 问题
- 很多团队在同一份代码里互相干扰;系统各部分的负载极不均匀。
- 思路
- 独立的服务,各自的数据库,各自部署。
- 例子
orders、inventory、delivery三个服务;黑五那天只伸缩订单和支付。- 代价
- 分布式系统:网络延迟、服务之间没有简单的事务、运维和调试都很复杂。
- 何时使用
- 大团队、不均匀的负载。对一个五个人的创业公司来说是杀鸡用牛刀。
- 关联
从 Modular Monolith 长出来;通过 Event-driven 和 Queues 通信;跨服务的操作需要 Saga。
Serverless —— 按需运行的函数
与其养一辆闲着的车,不如叫出租车,只在计价器跑着的时候付钱。
你写一些小函数(functions),云在事件发生时运行它们,自己从零伸缩到上千个副本再缩回去,按每次运行收费。
- 问题
- 服务器闲着也要花钱,而负载是一阵一阵来的。
- 思路
- 代码只在事件发生时运行;伸缩交给云。
- 例子
- 图片上传到存储 → 一个函数生成缩略图 → 两次上传之间什么都不运行。
- 代价
- Cold start(首次运行的延迟)、运行时间限制、被云厂商绑定。
- 何时使用
- 断断续续、由事件触发的活儿:处理文件、webhook、定时任务。
- 关联
- 天然和 Event-driven 搭配。
分布式系统把函数调用变成网络调用。它们每一个都可能失败。
一旦部分不止一个,架构就大半在它们之间的连线里了。它们怎么对话,决定了它们怎么出故障。
Request–Response —— 问完就等
在柜台点单:你问,你站着等,拿到咖啡才往前走。
客户端发出请求(request),然后等待响应(response)。这是同步通信:REST、gRPC、一次数据库查询。
- 问题
- 要继续往下走,现在就需要一个答案。
- 思路
- 调用 → 等结果 → 继续。
- 例子
- 结账时调用支付 API,等待确认。
- 代价
- 时间上的耦合:服务器慢了或宕了,调用方也跟着卡住。
- 何时使用
- 默认选项——当你真的需要立刻拿到答案时。
- 关联
- 和异步的 Event-driven、Queues 正好相反。
Event-driven Architecture —— 宣布它,让监听者各自反应
一间新闻编辑室:新闻一出,体育、天气、广告各自做出反应。播报员不等他们,甚至不知道谁在听。
组件不去调用别人,而是发布一个事件(event)——一个已经发生的事实:"OrderPlaced"。谁感兴趣,谁就订阅并做出反应。两种形式:broker(事件自由地传开)和 mediator(由一个协调者按顺序推进各步)。
- 问题
- 一个服务不得不知道并调用所有关心"某事发生了"的服务。
- 思路
- 发布事实;订阅者自己、各自独立地反应。
- 例子
- "OrderPlaced" → 给客户发邮件、在仓库预留、写入分析。
- 代价
- 流程难以追踪和测试;事件可能来两次,或者顺序错乱。
- 何时使用
- 一个动作引发很多互相独立的反应;副作用很慢。
- 关联
运行在 Queues / Pub-Sub 之上;是 Event Sourcing 和 Saga 的基础。
Message Queue 与 Pub/Sub —— 各部分之间的邮局
邮局替你存着信,直到收件人准备好。你把信扔进邮筒就走了;他醒来时再读。
**消息队列(message queue)**或 pub/sub broker 站在发送方和接收方之间,这样它们不必同时在线。在队列里,一条消息由一个 worker 取走;在 pub/sub 里,一条消息会发给所有订阅者。
- 问题
- 慢活儿拖慢了给用户的响应;负载高峰压垮系统。
- 思路
- 把任务放进队列,稍后按自己的节奏处理。
- 例子
一份很重的报表被放进队列(RabbitMQ、Kafka、SQS),worker 在后台生成它,网站保持飞快。
- 代价
- 结果变成延后的;要考虑重试和重复消息。
- 何时使用
- 后台任务、削平高峰、服务之间的通信。
- 关联
- 是 Event-driven 和 Microservices 的传输层。
Pipe-and-Filter —— 给数据装一条流水线
一条灌装线:洗瓶、灌装、封盖、贴标。每个工位只做一件事,然后把瓶子传下去。
数据流过一串独立的步骤——过滤器(filters),由**管道(pipes)**连接起来。每个 filter 只做一种转换。
- 问题
- 复杂的数据处理变成了一个巨大的函数。
- 思路
- 拆成简单的步骤,每一步有输入、有输出。
- 例子
grep | sort | uniq、一个 ETL 作业、一个编译器(分词 → 解析 → 优化 → 生成)。- 代价
- 当各步需要回头、或者互相商量时,就不太合适。
- 何时使用
- 工作本身是一串清晰的转换。
- 关联
- 各步之间常常用 Queues 连接。
同步是打电话。异步是写信。
这些模式不是关于盒子的,而是关于你怎么写、怎么读、怎么保存那份真相。
CQRS —— 写模型和读模型分开
图书馆有一份用来找书的快速目录,还有一间单独的后屋,把书归档上架。查找为一件事优化,存放为另一件事优化。
CQRS(Command Query Responsibility Segregation)把写模型——改变数据的命令(commands)——和读模型——取出数据的查询(queries)——分开。每一边都可以单独优化、单独伸缩。
- 问题
- 一个数据模型既不适合复杂的写入,也不适合快速的读取。
- 思路
- 两个模型:写模型带着所有规则,读模型是预先算好的"视图"。
- 例子
- 仪表盘从一个去规范化的视图里读,订单写进一个事务型数据库。
- 代价
- 两个模型要保持同步;读取可能稍有滞后(eventual consistency)。
- 何时使用
- 读比写多出好几倍,或者两者形状差别很大。不是默认选项。
- 关联
- 常常和 Event Sourcing 配对:日志里的事件用来构建读模型。
Event Sourcing —— 存历史,而不是存状态
银行不只存你的余额,而是存下每一笔收入和支出。余额只是它们的总和。丢了余额,按账本重算就行。
不去覆盖当前状态,而是存一条事件流(event log),只往末尾追加。当前状态通过**重放(replay)**事件得到。
- 问题
- 不仅要知道"现在是什么",还要知道"是怎么走到这一步的"。
- 思路
- 事件日志是真相来源;状态从它计算出来。
- 例子
账户:"开户、+100、−30、+5",而不是"余额:75"——完整的审计,还能回答"上周二是什么样?"。
- 代价
- "现在是什么"的查询更难了;事件不能修改——历史是永久的。
- 何时使用
- 金融、审计、复杂的业务流程。
- 关联
- 几乎总是和 CQRS 一起用;天然和 Event-driven 搭配。
Saga —— 一个可以倒回去的长操作
订一趟旅行:机票、酒店、租车。租车没订成——你就取消酒店和机票。没有一个"全部撤销"的按钮,只能一步一步倒回去。
当一个业务动作要经过好几个各有自己数据库的服务时,没法做成一个共同的事务。Saga 是一串本地事务;某一步失败时,就运行补偿动作(compensations),撤销前面的步骤。
- 问题
- 一个操作涉及好几个服务,却没有共同的事务。
- 思路
- 按顺序执行各步,每一步都配一个出错时用的"撤销"步。
- 例子
- 创建订单 → 预留商品 → 扣卡;扣卡失败——释放商品,取消订单。
- 代价
- 每一步都需要补偿;各步之间数据暂时不一致。
- 何时使用
- 横跨 Microservices 的业务操作。
- 关联
- 需要 Microservices;各步常常通过 Event-driven 来协调。
一个事务是一颗意图的原子。当它横跨多个服务时,你得自己把这颗原子造出来。
这些模式是为某一种具体的痛而存在的。当你正好有这种痛时再去拿它们——一秒也别提前。
Microkernel —— 小核心加插件
一把电钻:一个马达,十几个钻头。钻还是那把钻——活儿需要什么就换什么钻头。
一个极简的**核心(core)提供基础,其余一切都以插件(plugins)**的形式加入,插件接到核心上,彼此互不依赖。
- 问题
- 功能很多,经常变,而且不同客户要的不一样。
- 思路
- 稳定的小核心 + 互相独立的插件。
- 例子
- 带扩展的 VS Code 和浏览器;一个保险系统,地区规则就是一个插件。
- 代价
- 要仔细设计插件 API 和它们的兼容性。
- 何时使用
- 平台型产品,按客户定制。
- 关联
- 插件常常像 Hexagonal 那样通过端口接入。
Space-based Architecture —— 数据放内存,没有瓶颈
一座体育场有二十个售票窗口,每个都有自己的一份座位图——而不是只有一个窗口排着长队。
当唯一的数据库在巨大负载下成为瓶颈时,工作被分给许多 processing units,每个都把数据放在内存里。内存在它们之间同步(data grid),数据稍后再异步写进数据库。
- 问题
- 一个数据库扛不住高峰时的并发负载。
- 思路
- 数据放在多个节点的内存里,节点之间同步,稍后再写数据库。
- 例子
- 限时秒杀,或者一场涌入大量同时出价的在线拍卖。
- 代价
- 非常复杂、非常贵;内存和数据库之间有丢数据的风险。
- 何时使用
- 只在真正巨大、忽高忽低的并发负载下。
- 关联
- 像 Queues 一样用到缓存和异步写入。
Peer-to-Peer —— 没有中心
一场百乐餐:每个人既是厨师又是客人。没有餐厅,没有服务员——这群人自己喂饱自己。
节点——peers——直接互相通信,没有中心服务器。每个节点同时是客户端和服务器。
- 问题
- 中心服务器是一个昂贵又脆弱的单点故障。
- 思路
- 参与者自己在彼此之间分担负载和数据。
- 例子
- BitTorrent:每个下载的人同时也在上传——人越多,网络越强。
- 代价
- 很难保证一致性、安全和控制。
- 何时使用
- 文件分发、区块链、不依赖中心主机的韧性。
- 关联
- 和 Request–Response 这类客户端-服务器形状正好相反。
冷门模式是药,不是保健品。冲着确诊的病去吃它。
模式是一套词汇,不是只能点一道菜的菜单。真实的系统会组合好几种——而最明智的一步,往往是最简单的那一步。
模式之间是有关联的
一栋房子同时用到承重墙、水管和电线——而不是"唯一正确的那种建筑模式"。
模式不会单独存在。模块化单体会长成微服务;微服务需要事件、队列和 saga;Hexagonal 和 Clean 是同一个想法;CQRS 和 Event Sourcing 成对出现。
一个真实的系统,可能对外是一个模块化单体,每个模块里是 Clean Architecture,界面用 Request–Response,慢吞吞的副作用走事件,重活前面挡一个队列。问题不是"哪个模式",而是**"哪些模式,放在哪里"**。
从最简单的形状起步
你不会为了盖一间小木屋去浇一座体育场的地基。
几乎每个系统都该作为 Modular Monolith 起步,并挣得它的复杂度。Microservices、CQRS、Event Sourcing、Space-based——都是对规模和团队大小问题的回答,而这些问题你也许永远不会有。最便宜的分布式系统,是你没造的那个。
- 这个形状解决的是哪个反复出现的问题? 用一句话说出来。
- 我现在有这个问题吗——还是在猜未来?
- 它要花多少代价? 在延迟、运维、复杂度、金钱上。
- 还能用的最简单的模式是哪个? 从它开始。
- 我能不能以后再用它,等痛真的冒出来?
- 服务比工程师还多。
- 一个用户动作要碰到五个服务和两个队列。
- CQRS 或 Event Sourcing 比读取或审计问题来得还早。
- 团队里没人能凭记忆把系统画出来。
- 选这个模式,只因为大公司都这么做。
速查表:全部 17 个模式
组织单个应用
- Layered分层, n-tier
代码按楼层摆放:表现 → 逻辑 → 数据 → 数据库。请求从上往下走。
- Modular Monolith模块化单体
一个应用,切成接口清晰的模块。最好的默认选项。
- MVC / MVVMModel–View–Controller
在界面和数据与规则之间立一道墙。
依赖的方向
- HexagonalPorts and Adapters
核心声明端口,技术通过适配器接入。
- Clean / Onion整洁 / 洋葱
同心圆,所有依赖都指向中心(dependency rule)。
拆成多个部分
- Microservices微服务
独立的服务,各有数据库和部署。代价是分布式系统。
- ServerlessFaaS
函数由事件触发,云负责伸缩,按运行付费。
各部分怎么对话
- Request–Response同步调用
问完就等答案。简单,但在时间上耦合。
- Event-driven事件驱动
发布一个事实,订阅者自己做出反应。
- Message Queue / Pub-Sub消息队列
任务在队列里等着,直到有 worker 空出来。
- Pipe-and-Filter管道-过滤器
数据穿过一串简单的步骤。
数据
- CQRSCommand Query Responsibility Segregation
写和读用不同的模型。
- Event Sourcing事件溯源
存事件日志;状态就是它的重放结果。
- Sagasaga
跨多个服务的步骤 + 出错时的补偿。
特殊形状
- Microkernel插件架构
小核心加互相独立的插件。
- Space-based内存 data grid
数据放在多个节点的内存里,数据库不再是瓶颈。
- Peer-to-PeerP2P
节点直接通信,没有中心。
最好的架构,是那个能撑过你真实需求的、最简单的架构——再多一个模式都不要。