04 · 十大核心架构模式
模式不是用来炫技的咒语,而是前人替你踩过坑后留下的成熟解法。学模式,是为了手里有牌——遇到问题时,脑子里能立刻浮现几种打法,而不是从零硬想。
先说清楚:什么是架构模式,以及为什么要学它
你写代码会遇到这段逻辑似曾相识的感觉;架构也一样。同一类问题,在无数系统里反复出现:怎么把复杂度切开?怎么让系统在流量洪峰下不被冲垮?怎么让读和写互不拖累?
每当一个问题被足够多的人、用足够多的方式解决过,其中那些反复被证明有效的解法,就沉淀成了模式。模式是被验证过的套路,是社区的共识词汇。
学模式有两层价值:
- 手里有牌:遇到问题不至于裸奔。你知道这是个典型的读写分离场景,而不是对着白板发呆。
- 沟通带宽:你对同事说这里走事件驱动,他立刻懂你指的是哪一套结构、有哪些含义。模式是架构师之间的行话,一个词省下半小时解释。
但请把下面这句话刻在脑子里,它是本章的灵魂:
模式是工具,不是目标。 没有高级模式和低级模式之分,只有合不合适之分。 一个用对了的单体,远比一个用错了的微服务高级。
下面逐个过这十张牌。每张牌我都会讲三件事:①它解决什么问题 ②它长什么样 ③它的代价 / 什么时候千万别用。第三点最重要——看不到代价,就等于没学会这个模式。
1. 分层架构:Layered
① 解决什么问题
最朴素、最普遍的复杂度切分方式:把系统按关注点横向切成几层,每层只跟相邻层打交道。典型是三层——表现层管界面/接口,业务层管规则,数据层管存取。
它解决的是别把所有东西搅成一锅粥。界面逻辑、业务规则、数据库操作如果全揉在一起,改一处崩一片。分层给了你关注点分离:改界面不用碰数据库,换数据库不用动业务规则。
② 长什么样
┌─────────────────────────────┐
│ 表现层:Presentation │ 界面 / API,只管怎么展示和接收
├─────────────────────────────┤
│ 业务层:Business Logic │ 核心规则,这件事该怎么算
├─────────────────────────────┤
│ 数据访问层:Data Access │ 只管怎么存、怎么取
├─────────────────────────────┤
│ 数据库 / 外部存储 │
└─────────────────────────────┘
每层只调用下一层,不越级、不反向③ 代价 / 什么时候别用
- 代价:严格分层会带来穿透成本——一个简单查询可能要老老实实穿过每一层,写一堆只是把数据往下传的样板代码。层与层之间也容易出现贫血的传递对象。
- 什么时候别用:它几乎永远适用,所以问题不在用不用,而在别教条。不要为了纯粹而禁止一切合理的跨层优化;也别把分层误当成分布式——分层是代码组织方式,不代表每层都要是独立部署的服务。
分层是底座,不是全部。后面几个模式,很多是在分层的基础上,再针对某个质量问题做的专门处理。
2. 单体:Monolith —— 被严重低估的正确起点
① 解决什么问题
把整个应用作为一个可部署单元交付:所有模块在同一个进程里,一次构建、一次部署、一起上线。它解决的是怎么用最低的协作成本和运维成本,把东西先做出来、跑起来。
这里我要旗帜鲜明地表个态:单体被严重低估了。 过去十年微服务被炒得太热,以至于很多人一开口就嫌单体土、不先进。这是巨大的误解。
对绝大多数项目,单体是正确的起点,而且能撑得比你想象的久得多。 你熟悉的很多大产品,在用户过千万时跑的依然是一个组织良好的单体。
② 长什么样
┌───────────────────────────────────────────┐
│ 单 体 应 用:一个进程 │
│ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ 用户模块 │ │ 订单模块 │ │ 支付模块 │ … │ 模块之间是函数调用
│ └────────┘ └────────┘ └────────┘ │ 不是网络调用
│ │
└──────────────────────┬──────────────────────┘
│
┌────▼────┐
│ 数据库 │
└─────────┘注意:单体内部完全可以、也应该分模块。这叫模块化单体。单体不等于一团乱麻,它只是说这些模块部署在一起。
③ 代价 / 什么时候别用
- 代价:① 整个应用一起部署,改一行也要重新发布全部;② 无法对单个模块单独扩容,订单很忙、用户模块很闲时,也得整体一起加机器;③ 一个模块的内存泄漏/崩溃可能拖垮整个进程;④ 代码库变大后,如果模块边界不清,会退化成大泥球。
- 什么时候别用:当组织和规模真的到了单体扛不住的程度时,见下一节。但请记住:这些代价大多是成长的烦恼,是产品成功之后才需要面对的甜蜜负担。 在你还没验证产品有没有人用之前,就为这些问题买单,是典型的过度设计。
给新人的硬建议:默认从模块化单体开始。 等你真的痛了、痛在明确的某处了,再针对那一处拆。先拆了再说几乎总是错的。
3. 微服务:Microservices —— 被严重滥用的成熟期解药
① 解决什么问题
把一个大应用拆成多个可独立开发、独立部署、独立扩容的小服务,每个服务围绕一块业务能力,各自拥有自己的数据。它解决的核心问题其实不是技术问题,而是组织问题:
当一个团队大到几十上百号人,挤在一个单体里互相踩脚、谁也不敢动谁的代码、每次上线要全公司排队时,微服务让每个小团队拥有自己的服务、自己的发布节奏、自己的技术选择。
请记牢这句话:微服务首先是为了解决人的扩展性,其次才是机器的扩展性。
② 长什么样
┌──────────────┐
│ API 网关 │ 统一入口:路由、鉴权、限流
└──┬───┬───┬───┘
│ │ │ 每个服务 = 独立部署 + 独立数据库
┌─────▼┐ ┌▼────┐ ┌▼─────┐
│用户 │ │订单 │ │支付 │ 服务之间靠网络调用通信
│服务 │ │服务 │ │服务 │ 而不是函数调用
└──┬───┘ └─┬───┘ └──┬───┘
│ │ │
┌──▼─┐ ┌──▼─┐ ┌──▼─┐
│ DB │ │ DB │ │ DB │ 数据各自独立,不共享一个库
└────┘ └────┘ └────┘③ 代价 / 什么时候别用 —— 本章最重要的警告
微服务把单体里简单的函数调用,变成了复杂的网络调用。这一步跨越,引入了一整座分布式系统的复杂度大山:
- 网络会失败、会延迟、会乱序。原来一个
if就能解决的事,现在要处理超时、重试、幂等。 - 数据散落各处,跨服务的事务几乎不可能。还记得吗,跨库强一致是分布式系统最难的事,详见 05 · 数据与状态。你被迫接受最终一致,引入消息、补偿、Saga 等一堆机制。
- 运维复杂度爆炸:服务发现、链路追踪、统一日志、配置中心、容器编排……一个三人小团队,光是把这套基础设施搭起来就够呛,还没开始写业务。
- 本地调试变难:想跑通一个流程,可能要同时起七八个服务。
什么时候真的该用微服务?三个前提,最好同时满足:
┌───────────────────────────────────────────────────┐
│ 前提一:组织规模够大 │
│ 多个团队互相阻塞、共享代码库已成为协作瓶颈 │
│ │
│ 前提二:有明确的独立部署需求 │
│ 某些模块需要独立的发布节奏 / 独立的扩容 / 独立的可用性 │
│ │
│ 前提三:你已经有了承接复杂度的平台能力 │
│ 监控、追踪、CI/CD、编排都已就位,养得起这套基础设施 │
└───────────────────────────────────────────────────┘
三者都满足 → 考虑微服务
只是听说它先进 → 千万别滥用微服务的典型画面:一个五人团队,做一个月活几千的产品,却拆出了十几个微服务,每天大半精力耗在服务之间为什么调不通上,业务功能寸步难行。这不是先进,这是给自己上刑。
行业里有句话很扎心:微服务常常用来解决你还没遇到的问题,代价是制造一堆你本来不会有的问题。
4. 事件驱动:Event-Driven
① 解决什么问题
让组件之间通过发生了什么事来协作,而不是通过你去帮我做什么来命令。一个组件做完一件事,就广播一个事件,比如订单已支付。其他关心这件事的组件各自响应,而发出事件的人完全不知道、也不关心谁在听。
它解决的是解耦和可扩展业务流程:下单成功后要发短信、加积分、通知物流、更新报表……如果让下单服务挨个去调它们,它就和所有下游死死绑在一起,每加一个动作都要改下单代码。事件驱动让下单只管喊一嗓子:订单支付了!后续谁想加动作,自己来订阅就行。
② 长什么样
订单已支付事件
┌────────┐ 发布 ┌─────────────┐ 分发
│ 订单服务 │ ──────▶ │ 事件总线/ │ ──────┬────────┬────────┐
└────────┘ │ 消息中间件 │ │ │ │
发布者不关心 └─────────────┘ ▼ ▼ ▼
谁在消费 ┌──────┐ ┌──────┐ ┌──────┐
│发短信 │ │加积分 │ │通知物流│
└──────┘ └──────┘ └──────┘
订阅者各自响应,互不知情③ 代价 / 什么时候别用
- 代价:整体流程变得看不见。在命令式代码里,你顺着函数调用就能读懂下单后发生了什么;在事件驱动里,逻辑被打散到一堆订阅者中,没有一处能看到全貌,排查问题像破案。还要面对事件可能重复、乱序、丢失,以及最终一致带来的短暂不一致。
- 什么时候别用:① 流程简单、调用关系清晰时——硬上事件驱动只会把直路绕成迷宫;② 需要立刻拿到结果的同步场景,比如用户点了按钮要马上知道成败,事件驱动天然是异步的,不适合。
电商是事件驱动的经典舞台:订单支付一个事件,扇出库存、积分、物流、风控、报表一大片。见 电商平台模板。
5. 消息队列 / 异步处理
① 解决什么问题
在生产者和消费者之间放一个缓冲水池,也就是队列:生产者把任务扔进去就走,消费者按自己的节奏慢慢捞出来处理。它解决三个经典问题:
- 削峰填谷:洪峰来了,先把请求堆进队列,后端按自己能承受的速度消化,不会被瞬间冲垮。
- 异步解耦:耗时的活儿,比如发邮件、转码、生成报表,不必让用户在那儿干等,扔进队列,先告诉用户正在处理。
- 可靠投递:消费者挂了,任务还在队列里,重启后接着干,不丢。
② 长什么样
洪峰流量 后端按自己的节奏消费
▼ ▼ ▼ ▼ ▼ ┌────────────────────┐
┌─────────┐ │ ████████████░░░░░░ │ ┌──────────┐
│ 生产者 │───▶│ 消 息 队 列 │───▶│ 消费者:可 │
│快速堆积 │ │ 缓冲水池,先进先出 │ │ 多个并行 │
└─────────┘ └────────────────────┘ └──────────┘
水池吸收波动,把尖刺抹平成平缓水流同步 vs 异步的直觉:同步像打电话——你得等对方接、等对方说完;异步像发短信——发完就干别的,对方有空再回。
③ 代价 / 什么时候别用
- 代价:① 引入了一个必须维护、监控、且自身不能挂的关键基础设施;② 处理变成异步,用户拿不到即时结果,产品要设计处理中状态;③ 必须处理消息可能被消费两次——所以消费逻辑要做成幂等,也就是同一条消息处理一次和处理十次结果一样;④ 队列堆积本身会成为新的监控对象,积压了说明消费跟不上了。
- 什么时候别用:需要同步、即时返回结果的链路别硬塞队列;任务量很小、根本没有峰值压力时,加一个队列只是徒增运维负担。
视频转码是异步处理的典范:用户上传后,转码这种又慢又重的活必须丢进队列,后台慢慢转,不能让用户对着进度条干等。见 视频流媒体模板。
6. CQRS:读写模型分离
① 解决什么问题
CQRS = Command Query Responsibility Segregation,中文可理解为命令与查询职责分离。一句话:把写和读拆成两套独立的模型/路径,各自优化。
它解决的是读和写的诉求根本不一样,硬用同一个模型会两头不讨好:写要的是规则严谨、强一致、防止脏数据;读要的是快、灵活、能按各种维度聚合。很多系统读远多于写,比如商品详情页,一次修改、千万次浏览。用同一套模型扛,读会被写的约束拖累,写会被读的各种索引拖累。
② 长什么样
写请求:Command 读请求:Query
│ │
▼ ▼
┌─────────┐ 数据同步/事件 ┌────────────────┐
│ 写模型 │ ─────────────────▶ │ 读模型:可多个 │
│ 规则严谨 │ 常常是异步, │ 为查询预先优化好 │
│ 强一致 │ 最终一致 │ 反范式、可缓存 │
└────┬────┘ └───────┬────────┘
▼ ▼
写库:为正确性优化 读库/视图:为读取速度优化③ 代价 / 什么时候别用
- 代价:复杂度直接翻倍。两套模型要维护,中间的同步通道要可靠;而且读写之间通常是异步同步的——意味着你写完之后,读到的可能还是旧数据,也就是最终一致,产品和用户都要能接受这一点。
- 什么时候别用:绝大多数系统不需要 CQRS。 读写压力差不多、或者数据量根本没大到需要分开优化时,上 CQRS 是自找麻烦。它是读写严重不对称、且常规手段已经压榨干净之后的重武器,不是默认选项。
CQRS 和事件驱动是天生一对:写模型产生事件,读模型订阅事件来更新自己。社交信息流就常用这种思路——写,也就是发帖,和读,也就是刷 Feed,是完全不同的两套优化路径,见 社交信息流模板。
7. 发布-订阅:Pub/Sub
① 解决什么问题
Pub/Sub 是一种通信模式:发布者把消息发到一个主题 topic,所有订阅了该主题的订阅者都会各自收到一份。它和消息队列很像,但有关键区别:
- 队列:一条消息只被一个消费者处理,分活儿,你做了我就不做。
- Pub/Sub:一条消息被所有订阅者各收一份,广播,人人有份。
它解决的是一件事要通知很多方,且通知方不想知道有哪些接收方的彻底解耦。它是事件驱动在通信层面的常见实现机制。
② 长什么样
┌──── 订阅者 A:每人收到完整一份
┌────────┐ 发布到 │
│ 发布者 │ ─topic──▶ ├──── 订阅者 B
└────────┘ │
└──── 订阅者 C
对比队列:一条消息只给 A 或 B 或 C 其中一个,分摊任务③ 代价 / 什么时候别用
- 代价:① 和事件驱动一样,全局流程难以追踪,这条消息到底谁收了、谁处理成功了,需要额外的可观测手段;② 订阅者多了,投递的扇出成本上升;③ 可靠性语义,比如至少一次、至多一次,要想清楚,否则要么丢消息要么重复消费。
- 什么时候别用:点对点的、一对一的明确调用,用 Pub/Sub 是杀鸡用牛刀;强一致、要立即确认的交互也不适合广播式的异步通信。
Pub/Sub 是事件驱动的运输工具,消息队列是异步处理的运输工具——两者底层常是同一类中间件,区别在投递语义,也就是广播 vs 分摊。
8. 客户端-服务端 / BFF:为前端裁剪的后端
① 解决什么问题
客户端-服务端是最基础的分工:客户端管交互和展示,服务端管数据和逻辑。而 BFF,也就是 Backend For Frontend,是它的一个精致升级:为每一种前端,单独做一个贴身的后端。
它解决的问题是:Web、iOS、Android、智能手表……不同端的屏幕、网络、交互千差万别,它们想要的数据形状和聚合方式也不同。如果让一个通用 API 同时伺候所有端,要么接口臃肿,什么字段都给,移动端流量遭罪;要么客户端被迫自己发好几个请求东拼西凑,慢、费电。BFF 让每个端都有一个专属管家,把后端的零散数据裁剪、聚合成这个端最顺手的形状。
② 长什么样
┌────────┐ ┌────────┐ ┌────────┐
│ Web │ │ iOS │ │ 第三方 │ 不同端,不同诉求
└───┬────┘ └───┬────┘ └───┬────┘
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Web BFF │ │移动 BFF │ │ 开放API │ 每个端一个贴身后端
└───┬────┘ └───┬────┘ └───┬────┘ 负责裁剪/聚合
└────────────┼────────────┘
▼
┌───────────────────────┐
│ 后端核心服务 / 微服务 │ 核心逻辑只写一份
└───────────────────────┘③ 代价 / 什么时候别用
- 代价:多了一层要开发和维护的中间人;如果团队不大,几个 BFF 之间容易出现重复代码;BFF 本身也可能膨胀成新的小单体。
- 什么时候别用:只有一种前端、或者各端诉求高度一致时,加 BFF 纯属多此一举——一个通用 API 就够了。BFF 是多端 + 各端差异大 + 后端是多个服务时才划算。
AI 对话产品里,编排层某种程度上就是一个重型 BFF:它把推理、检索、工具、会话等一堆后端能力,裁剪成前端要的那一段流式对话。流式输出 SSE 也是典型的客户端-服务端协作模式。见 AI 对话产品模板。
9. 管道-过滤器:Pipeline
① 解决什么问题
把一个处理任务拆成一串首尾相接的处理步骤,也就是过滤器。数据像在流水线上一样,一站一站被加工,前一站的输出就是后一站的输入。
它解决的是复杂的数据加工流程,怎么拆得清晰、可复用、可独立替换。每个过滤器只干一件小事、只关心自己的输入输出,对上下游一无所知。这样每一步都能独立开发、独立测试、独立替换、甚至独立扩容;想加一个新步骤,插进管线就行。
② 长什么样
原始输入 最终产物
│
▼
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ 校验 │──▶│ 清洗 │──▶│ 转换 │──▶│ 压缩 │──▶│ 入库 │──▶
└──────┘ └──────┘ └──────┘ └──────┘ └──────┘
每个过滤器只管自己这一站,不知道上下游是谁
→ 任意一站都可单独替换、单独加机器③ 代价 / 什么时候别用
- 代价:① 整条管线的吞吐受最慢的那一站拖累,也就是木桶效应;② 一站失败,要想清楚整条链路怎么回滚或重试;③ 数据在各站之间传递、序列化也有开销;④ 不适合需要站与站之间来回交互的逻辑——管道是单向流水,不是对话。
- 什么时候别用:逻辑本身不是线性流水形态时硬套管道,会很别扭;步骤之间高度耦合、需要频繁互相回头看的场景也不适合。
视频转码是教科书级的管道:上传 → 切片 → 转多种码率 → 打包 → 分发 CDN,一站接一站。任何数据加工 / ETL / 媒体处理流程都天然是管道形态。见 视频流媒体模板。
10. 微内核 / 插件化:Microkernel / Plugin
① 解决什么问题
把系统分成两部分:一个稳定的、最小的内核,也就是核心;加上一堆可插拔的插件。内核只提供最基础、最不变的能力,以及一套插件怎么接进来的规则;具体的、多变的、个性化的功能,全部做成插件。
它解决的是核心稳定、外围多变这类系统的扩展性:你希望第三方,甚至用户自己,能在不动核心代码的前提下,扩展系统能力。浏览器和它的扩展、IDE 和它的插件、各种应用市场生态,都是这个模式。
② 长什么样
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 插件 A │ │ 插件 B │ │ 插件 C │ ← 多变、可热插拔
└─────┬────┘ └─────┬────┘ └─────┬────┘ 可由第三方开发
│ │ │
┌─────▼────────────▼────────────▼─────┐
│ 稳 定 的 内 核:Core │ ← 最小、最稳定
│ 只提供基础能力 + 插件接入规则:契约 │ 很少改动
└──────────────────────────────────────┘③ 代价 / 什么时候别用
- 代价:① 那套插件契约/接口一旦定下来就很难改,改了所有插件都得跟着动,设计时要极其谨慎;② 插件质量参差不齐,一个烂插件可能拖垮甚至搞崩内核,所以常需要隔离/沙箱;③ 内核要预留扩展点,本身设计难度高。
- 什么时候别用:功能稳定、没有让外人扩展需求的系统,搞插件化是过度设计——你只是给自己增加了一套用不上的复杂机制。
浏览器插件就是微内核思想的活样本:浏览器是内核,扩展在受限沙箱里运行、通过规定好的接口接入,既扩展能力又不能为所欲为。见 浏览器插件模板。
怎么选?一个简易决策提示
讲完十张牌,最怕你转头就开始集邮——这个项目我要把模式都用上。打住。 选模式只需要顺着问几个问题:
┌─────────────────────────────────────────┐
│ Step 0:能不用模式就先别用 │
│ 最简单的分层单体能不能解决?能 → 就这样 │
└────────────────────┬────────────────────┘
│ 解决不了,才往下问
▼
┌──────────────────────────────────────────────────────────┐
│ 问 1:我遇到的到底是什么问题?对症,别对时髦 │
│ • 代码搅成一团 ───────────────▶ 分层 / 模块化 │
│ • 一件事要扇出通知很多方 ───────▶ 事件驱动 / Pub/Sub │
│ • 有耗时任务 / 要削峰 ──────────▶ 消息队列 / 异步 │
│ • 读写诉求严重不对称 ───────────▶ CQRS │
│ • 多端、各端形状差异大 ─────────▶ BFF │
│ • 线性数据加工流程 ─────────────▶ 管道-过滤器 │
│ • 核心稳定、要让外人扩展 ───────▶ 微内核 / 插件 │
│ • 多团队互相阻塞、组织扛不住 ────▶ 慎重考虑微服务 │
└────────────────────────────────┬─────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ 问 2:这个模式的代价我现在付得起、也愿意付吗? │
│ 付不起 / 问题还没真出现 ──▶ 先别上,记一笔以后可能要 │
└──────────────────────────────────────────────────────────┘三条心法,送给你:
- 从简单开始,被痛驱动地演进。 不要预支未来的复杂度。等问题真的出现、且痛在明确的某处,再引入对应模式。
- 模式可以组合。 真实系统几乎都是分层单体打底,局部用上事件驱动、异步、缓存。模式不是单选题,而是在合适的局部用合适的牌。
- 永远能说出代价。 你引入任何一个模式,如果说不清它换走了我的什么,比如简单性、一致性、可观测性,那你大概率是在跟风,不是在做架构。
📌 真实案例:这些模式,谁在用
模式不是教科书概念,每一个背后都有真实系统在用:
- 单体 / 模块化单体 → Shopify(280 万行 Ruby)、Stack Overflow、Basecamp/DHH
- 微服务及其务实回退 → Netflix 是标杆;但 Amazon Prime Video 和 Segment 都从微服务回到了单体
- 事件驱动 / 消息队列 → 电商模板 的下单扇出、通知系统
- CQRS / 读写分离 → 社交信息流、短链接
- 管道 - 过滤器 → 视频转码;微内核 / 插件 → 浏览器插件
该用哪个、谁又用错了的真实对比,见 09 · 架构品味。
本章小结
- 模式 = 对反复出现的问题的成熟解法。 学它是为了手里有牌和沟通有共同语言,不是为了显得高级。
- 十张牌各有各的适用问题和代价:分层切关注点、单体是被低估的正确起点、微服务是被滥用的成熟期解药、事件驱动 / Pub/Sub 做解耦与扇出、消息队列 / 异步做削峰与解耦、CQRS 应对读写不对称、BFF 服务多端、管道做线性加工、微内核做可扩展生态。
- 最重要的两句话:① 模式是工具不是目标,别为了用而用;② 微服务被严重滥用——它首先解决人的扩展,不是机器的扩展;在组织规模、独立部署需求、平台能力三个前提没满足前,默认用模块化单体。
- 选模式的口诀:能不用就先别用 → 对症下药 → 付得起代价才上。
🎯 随堂检验
学完这十张牌,做几道题检验一下。点选项即时看对错:
- A机器性能不够、扛不住流量
- B团队 / 组织协作的扩展性:多团队互相阻塞、谁也不敢动谁的代码
- C让代码看起来更优雅、更先进
- A微内核 / 插件化
- BCQRS:读写模型分离
- C管道 - 过滤器
承上启下:这十个模式里,凡是涉及一致性、最终一致、事务、数据放哪的地方,比如微服务、事件驱动、CQRS,背后都指向同一个系统中最硬的骨头——数据与状态。逻辑好改,数据难改。下一章 05 · 数据与状态,我们就去啃这块真正的难点。
💬 评论