怎样完成一次系统设计:从业务约束到架构方案
用一个活动报名系统贯穿需求澄清、容量估算、数据建模、读写路径、故障语义、可观测性与验证,说明架构图中的每个组件应当怎样从约束中推导出来。
“设计一个高并发活动报名系统”看起来是一道架构题,很多答案却从组件清单开始:入口放 CDN 和网关,中间加 Redis 与消息队列,后端拆成微服务,数据库做主从和分库分表。图可以画得很满,但没人说明峰值有多高、报名成功的定义是什么、名额能否超发、故障时给用户返回什么,也就无法判断这些组件是否解决了问题。
系统设计的产物是一组可以检查的决策,拓扑图只是其中一种表达。业务目标决定系统必须守住哪些事实,流量和延迟决定资源预算,数据模型决定并发冲突出现在哪里,故障语义决定超时后能否重试,观测与实验则负责证明方案在预期负载和故障下成立。组件是这些决定之后的实现手段。
本文用一个活动报名系统走完这条推导链。用户可以浏览活动,在开放时间提交报名;每场活动名额有限,同一用户只能成功一次。运营人员能够查看报名结果并关闭活动。文章建立一种可复用的设计方法,不把某套生产架构当作通用答案。后续设计短链接、秒杀、支付、Feed 或文件服务时,仍可以使用同一条推导链。
一、先定义设计问题,别急着选技术
拿到需求后,第一步是把名词改写成可验证的行为。“高并发”“高可用”“实时”都不是完整要求。高并发可能是全天稳定的 2 万 QPS,也可能是开场十秒内的 20 万 QPS;实时可以是 100 ms 内可见,也可以是五秒内最终更新;高可用也可能只要求浏览可用,而报名写入在数据不确定时必须拒绝。
对活动报名系统,至少要先确认以下问题:
| 维度 | 需要确认的内容 | 本文采用的假设 |
|---|---|---|
| 用户动作 | 浏览、报名、取消、查询分别怎样使用 | 浏览和查询高频,报名集中在开放瞬间,暂不支持取消 |
| 规模 | 日活、活动数、峰值请求、热点分布 | 100 万用户关注同一活动,开放前后出现尖峰 |
| 正确性 | 哪些事实绝不能出错 | 不超发;同一用户不重复占名额;成功结果不能消失 |
| 延迟 | 用户能接受多长等待 | 浏览 p99 小于 200 ms;报名 1 s 内返回已成功、已满或处理中 |
| 可用性 | 哪些路径可以降级 | 活动页可返回短时间旧数据;无法确认名额时不能伪造成功 |
| 恢复 | 故障后允许丢多少、多久恢复 | 已确认报名 RPO 为 0;核心报名路径 RTO 目标 10 分钟 |
| 成本 | 能否为短峰长期保留大量资源 | 常态控制成本,开场前预扩容,非核心读取允许降级 |
这些假设会直接改变方案。如果允许少量超发后人工处理,库存可以使用更弱的并发控制;如果报名资格涉及付款,结果未知时还要增加查询、补偿和对账;如果一场活动只有一千人访问,单库事务可能已经足够。设计时应把假设写在图旁边,因为任何数字和边界变化都可能推翻后面的结论。
先写不变量,再写功能列表
功能列表说明系统能做什么,不变量说明系统无论并发和故障怎样发生都不能破坏什么。本文的三个核心不变量是:
I1: confirmed_count(event_id) <= capacity(event_id)
I2: unique(event_id, user_id)
I3: API 已返回 CONFIRMED 的记录之后仍可查询到
第一条约束名额不超发,第二条约束用户不重复占位,第三条约束成功响应与持久化承诺。它们应该落实为数据库条件更新、唯一约束和提交顺序,而不能只依赖应用层先查后写。缓存、消息队列和负载均衡都可以提高性能,却不会自动维护这些业务事实。
这张图的阅读方向是从左到右。需求先变成 SLO 与不变量,然后才换算容量、定义状态和读写路径。每加入一个组件,都要能沿箭头回到它负责的约束;找不到来源的组件通常是习惯性复杂度。
二、用 SLO 把“好用”变成系统边界
系统设计需要给性能和可靠性一个停止条件。否则团队只能不断追求更快、更稳,成本没有上限,也无法判断一次优化是否值得上线。服务等级目标(SLO)适合描述在一段窗口内希望达到的用户体验,例如:
活动详情:99.9% 请求成功,p99 < 200 ms
报名提交:99.95% 请求在 1 s 内返回终态或 PROCESSING
结果查询:99.9% 请求成功,p99 < 300 ms
已确认记录:不允许因单机故障丢失
平均响应时间不能替代分位数。九成请求耗时 30 ms、剩余一成耗时 3 s,平均值可能看起来尚可,真实用户却会持续碰到长尾。Google SRE 的 SLO 章节也强调延迟应作为分布观察,并根据不同请求类别设置目标。浏览和报名的成本、风险不同,不能用同一个总体平均值掩盖报名路径的退化。
SLO 还要对应失败行为。报名服务没有在 1 s 内确认结果时,可以返回 PROCESSING 和查询凭证;如果请求确定未进入系统,可以返回明确失败;如果结果未知,不能让客户端换一个请求号盲目重试。接口契约必须区分成功、明确失败和结果未知,否则高并发时任何超时都会变成重复请求放大器。
三、容量估算的目的,是暴露数量级错误
容量估算用来发现方案里的数量级问题,白板阶段无法给出精确机器数。日均请求很少并不代表峰值低;存储容量足够也不代表写入吞吐足够;CPU 空闲也可能因为数据库连接或热点行锁而无法继续处理。
假设一场活动有 100 万关注用户,开放前后 10 秒内有 30% 用户点击报名,其中客户端因页面刷新和网络重试产生 1.3 倍请求。入口峰值近似为:
报名峰值 QPS = 1,000,000 × 30% × 1.3 ÷ 10s
= 39,000 QPS
这个结果只是入口容量。一次报名可能触发资格查询、幂等检查、库存条件更新、报名记录写入和事件发布。若每个入口请求平均产生三次数据库访问,数据库面对的是约 11.7 万次操作每秒。若所有请求都读取同一行活动库存,这些压力会集中在一个锁冲突点上,无法均匀分布到数据库集群。
QPS、并发数和响应时间必须一起看
Little’s Law 在稳定系统中给出一个简单关系:平均在途数量等于有效到达速率乘以平均停留时间。
L = λ × W
39,000 requests/s × 0.1s = 3,900 个在途请求
39,000 requests/s × 1.0s = 39,000 个在途请求
流量没有变化,响应时间从 100 ms 增长到 1 s,在途请求会扩大十倍。这些请求占用连接、内存、队列位置和下游并发。下游变慢后的在途工作累积是常见的崩溃起点,超时和重试会继续放大负载。
资源预算要按瓶颈分别计算。应用实例关注 CPU、内存、连接和事件循环;数据库关注事务吞吐、锁等待、日志落盘与连接数;缓存关注操作吞吐、网络带宽、大 key 和热点 key;消息队列关注生产速率、消费速率、分区并行度和积压磁盘。只用“每台机器扛 5,000 QPS”概括所有请求,会忽略不同接口可能相差几个数量级的成本。
峰值、冗余和发布要进入同一份预算
若压测证明单实例在目标 p99 下可处理 1,500 QPS,39,000 QPS 理论上需要 26 个实例。生产配置不能直接写 26,因为滚动发布会下线实例,机器也可能同时故障,流量还会分布不均。假设目标利用率为 60%,并预留两个实例同时不可用:
基础实例数 = ceil(39,000 / 1,500) = 26
利用率余量 = ceil(26 / 0.60) = 44
故障与发布 = 44 + 2 = 46
这里的数字只演示推导过程。Google SRE 的生产服务实践建议通过负载测试建立资源与服务容量的关系,并在峰值容量之外考虑同时发生的计划内和计划外下线。容量必须绑定目标延迟;如果单实例只有在 p99 达到 4 s 时才完成 1,500 QPS,它并不满足本文的容量定义。
图中每一层都会产生放大或收缩。缓存命中会减少数据库读取,重试会增加下游调用,批处理可以降低每条消息的固定成本,热点则让集群总容量无法被均匀使用。容量评估要记录这些倍率,后续压测再用实测结果替换假设。
四、先设计最小数据模型,冲突位置才会显现
架构图无法替代数据模型。报名系统至少需要活动和报名两类持久状态:
CREATE TABLE event (
event_id BIGINT PRIMARY KEY,
capacity INT NOT NULL,
confirmed_count INT NOT NULL,
status VARCHAR(16) NOT NULL,
open_at TIMESTAMP NOT NULL,
version BIGINT NOT NULL
);
CREATE TABLE registration (
registration_id BIGINT PRIMARY KEY,
event_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
request_id VARCHAR(64) NOT NULL,
status VARCHAR(16) NOT NULL,
created_at TIMESTAMP NOT NULL,
UNIQUE KEY uk_event_user (event_id, user_id),
UNIQUE KEY uk_request (request_id)
);
uk_event_user 把“一人一次”交给数据库唯一约束,request_id 识别同一次客户端意图的重试。confirmed_count 的条件更新维护总量:
UPDATE event
SET confirmed_count = confirmed_count + 1,
version = version + 1
WHERE event_id = :event_id
AND status = 'OPEN'
AND confirmed_count < capacity;
更新影响一行表示拿到名额,影响零行表示活动已满或不开放。这个方案的正确性清楚,但热门活动的所有写都竞争同一行,吞吐可能很低。现在我们准确找到了冲突点,才有理由讨论库存分桶、排队串行化或预分配额度。若一开始就做分库分表,却仍让所有请求更新同一个逻辑库存,水平拆分并没有消除热点。
状态机定义哪些转移可以重放
报名记录不能只有一个布尔值。请求写入后,进程可能在扣减名额成功、插入报名记录之前崩溃;消息可能发送失败;客户端也可能没有收到响应。可以使用下面的最小状态机:
RECEIVED -> CONFIRMED
-> REJECTED_FULL
-> REJECTED_INELIGIBLE
-> PROCESSING_RECONCILIATION
状态转移要说明由谁执行、条件是什么、能否重试、超时后怎样查询。CONFIRMED 和明确拒绝是终态;PROCESSING_RECONCILIATION 表示系统接受了请求但尚未确认业务事实,需要后台任务继续核对。用户查询必须读取权威报名记录,不能只看某个进程内存中的 Future。
五、从单体基线开始,再按瓶颈拆分
第一版完全可以由一个应用和一个关系数据库组成。浏览接口读取活动表,报名接口在本地事务里执行唯一性检查、条件扣减和报名插入。对低流量活动,这套方案简单、正确、容易运维。系统设计不要求一开始拥有最多组件,而要求当前复杂度与约束相称。
单体基线还提供可比较对象。压测发现瓶颈之前,无法知道加入 Redis 或 MQ 改善了哪一段。若 CPU 已耗尽,给活动详情加缓存可能有效;若瓶颈是热点库存行锁,扩容无状态应用不会提高报名吞吐;若数据库日志盘已满,读写分离也不能加速主库写入。
演进顺序可以按证据推进:
- 浏览读取占据数据库大部分负载,且允许短时间陈旧,再加入活动详情缓存。
- 应用 CPU 或连接达到目标利用率,通过无状态实例和负载均衡水平扩展。
- 热门活动库存行成为写入瓶颈,再选择排队、库存分桶或额度预分配。
- 通知等后置动作拉长主链路,使用事务 Outbox 与消息队列异步执行。
- 单库容量、维护窗口或故障域成为限制,再考虑分片、复制和多机房。
每一步都要写出触发证据、预期收益、新增失败模式和回滚方式。这样架构演进是一组可审查的实验,而不是一次把所有流行组件搬进系统。
六、把读取路径和写入路径分开设计
活动详情是读多写少的数据,可以缓存;报名结果是用户自己的关键状态,需要明确新鲜度;名额计数既是热点,又参与正确性判断。把三者都当成普通 key-value 读取,会让缓存承担不该承担的权威职责。
读取路径允许多快、多旧
活动标题、图片、规则和开放时间适合使用 Cache Aside。请求先读 Redis,未命中再回源数据库并写缓存。运营修改活动后可以先提交数据库,再删除缓存;缓存还要设置过期时间作为兜底。详情页显示的“剩余名额”可以是近似值,但要明确标注并且不能据此承诺报名成功。
开放瞬间,热门 key 失效会让大量请求同时回源。可以在开放前预热,使用逻辑过期配合单个刷新者,或者把静态内容放到 CDN。空活动 ID 应使用参数校验、空值短缓存或布隆过滤器抑制穿透。缓存一致性、穿透、击穿和雪崩会在本系列后续文章单独展开;这里需要记住的边界是:缓存服务读取性能,数据库约束或单写者维护报名事实。
报名结果查询可以先读数据库或一份按提交顺序更新的可靠读模型。若使用缓存,成功写入后要确保查询不会长期返回“未报名”。常见方式包括写后删除缓存、带版本号更新,以及让客户端携带刚刚提交得到的 registration ID 查询权威路径。具体选择取决于读写比例和允许的陈旧窗口。
写入路径围绕提交点组织
报名写入的关键是承诺成功的时刻。使用单库事务时,提交点是名额条件更新与报名记录一起提交;使用队列串行化时,消息写入成功可能只表示“已受理”,消费者提交报名记录后才是 CONFIRMED;使用预分配额度时,还要定义额度和中心库存怎样对账。服务数量本身不能说明这个提交点。
下面是一条保守的同步路径:
1. 网关验证身份、活动和基本参数
2. 报名服务根据 request_id 查询已有结果
3. 开启数据库事务
4. 插入 registration,依赖唯一键拒绝重复用户
5. 条件更新 event.confirmed_count
6. 名额不足则回滚并记录明确拒绝
7. 成功则写入 outbox 事件并提交事务
8. 返回 CONFIRMED,异步发送通知
如果第 4 步先插入,而第 5 步发现已满,事务回滚即可。若希望保存拒绝记录,可以在事务外按同一个 request ID 写入终态,但要避免与重试并发产生两份结论。outbox 与报名记录在一个本地事务提交,消息发布器稍后投递,避免数据库成功而通知事件永久丢失。
图中的实线主链路负责给出报名结果,虚线表示可以异步完成的通知和读模型更新。Redis 没有位于名额提交点上,因为本例选择数据库条件更新维护不变量。若压测证明热点行无法达到目标吞吐,下一版可以改变写入模型,但必须重新证明 I1 到 I3。
七、异步化只能移动等待,不能消灭工作量
消息队列通过缓冲和生产消费解耦来“削峰”。若入口以 4 万条每秒持续写入,而消费者只能处理 1 万条每秒,每秒会新增 3 万条积压。队列让入口暂时不阻塞,最终仍要扩容消费者、降低生产速率或接受更长完成时间。
报名是否适合全异步,取决于产品语义。用户提交后立即返回“排队中”,消费者按顺序分配名额,可以把数据库写峰摊平,也能让同一活动由单个逻辑分区串行处理。代价是用户不能立即知道是否成功,队列积压会延长等待,消费者重复投递要求幂等,分区故障还会阻塞某些活动。
有些动作天然适合异步:发送短信、生成运营报表、刷新搜索索引、同步分析系统。它们失败不应回滚已经确认的报名。名额确认是否异步则是业务决策,不能因为“高并发就上 MQ”自动得出。后续的消息队列文章会计算生产速率、消费速率、积压年龄和扩容时间,并讨论什么时候队列只是推迟故障。
八、热点决定系统的有效容量
一百个活动均匀接收 4 万 QPS,与一个活动独占 4 万 QPS 是两种系统。前者可以按 event_id 分区并行处理,后者会集中访问同一缓存 key、同一库存状态和同一个消息分区。集群总 CPU 仍有空闲,也可能因为一个分片先到上限而拒绝请求。
解决热点需要先确定热点资源:静态活动详情可以多级缓存和请求合并;精确名额计数可以分桶后汇总,也可以进入按活动分区的单写者;热门用户账户若需要扣减余额,则可能要求按用户键串行处理。不同热点不能用同一种“加机器”解决。
库存分桶示例:一万个名额拆成一百个桶,每桶一百份,用户按稳定哈希选择桶;当前桶耗尽后再尝试少量其他桶。这样写冲突从一行分散到一百行,但会引入额度分配不均、跨桶重试和最终汇总。分桶总量仍必须等于活动容量,扩缩桶也不能凭空创建额度。它是用更复杂的状态管理换写并行度,只有单行确实成为瓶颈时才值得使用。
九、故障语义要沿请求路径逐跳定义
架构评审不能只画正常箭头。报名服务调用数据库超时,可能是事务没有执行,也可能已经提交但响应丢失。若服务直接返回失败,客户端换一个 request ID 重试,唯一用户约束或许阻止重复占位,但用户体验会在“失败”和“其实成功”之间矛盾。
每个外部调用都要写出四项:deadline、可重试错误、幂等依据和结果查询方式。入口的端到端 deadline 为 1 s,下游超时总和必须更短,给代码执行与响应留出余量。只有连接建立前失败等能够确认未执行的情况,才容易安全重试;结果未知时应使用原 request ID 查询。
故障行为也要按依赖分层:
| 故障 | 可以做什么 | 不能做什么 |
|---|---|---|
| 活动详情缓存不可用 | 限速回源、返回短时间旧数据 | 让所有请求同时击穿数据库 |
| 报名数据库延迟升高 | 限制在途并发、返回处理中或明确拒绝 | 排入无界内存队列继续等待 |
| 通知队列不可用 | 保留 outbox,稍后补发 | 回滚已确认报名或伪造未报名 |
| 结果查询读副本延迟 | 路由权威主库或携带版本等待 | 把旧副本的“无记录”当最终失败 |
| 单个实例故障 | 负载均衡摘除并转移新请求 | 假设进程内未完成工作自动恢复 |
现有的过载治理文章会详细说明限流、并发控制、隔离、熔断、降级和背压各自位于哪里。系统设计阶段要先给这些机制留下明确接口:错误分类、请求 deadline、幂等键、业务降级结果和资源配额。
十、分片和微服务要由独立扩展边界驱动
报名表增长到单库无法容纳或写入吞吐超过单主库能力时,可以按 event_id 分片,使同一活动的报名与库存位于同一分片。这样活动内事务简单,热点活动却仍集中在一个分片。按 user_id 分片能分散用户写入,但统计某个活动和维护总名额需要跨片协调。分片键选择需要在主要查询、事务边界和热点分布之间取舍。
高并发系统可以先保持模块化单体。活动配置、报名交易和通知的变化频率、数据所有权、扩展方式与故障风险不同时,再逐步拆成独立服务。若一个小团队仍需同时发布所有模块,把本地函数调用变成 RPC 只会增加超时、重试、部署和观测成本,仍然没有获得独立性。
判断是否拆分可以问三个问题:它是否需要独立扩容,是否拥有清楚的数据与不变量,故障时是否应该与其他模块隔离。三个答案都含糊时,先维持模块化单体通常更容易保持正确性。AWS Well-Architected 的工作负载架构也把服务边界与分布式交互分开讨论;拆成服务之后,网络延迟和数据丢失成为必须处理的新故障面。
十一、安全与滥用成本属于主路径
活动报名面对的“高并发”可能来自真实用户,也可能来自脚本、重复点击和攻击。身份认证、资格校验、验证码、设备风险、用户级频率限制都在消耗资源。如果把它们全部放在数据库事务之后,恶意流量已经占用了最昂贵的连接和锁。
便宜且确定的校验应尽量靠前,例如请求格式、活动是否存在、开放时间和用户级频率;需要读取强一致资格或余额的判断仍要在权威路径完成。风控结果超时时也要定义语义:普通活动可以拒绝并提示稍后再试,付费或高价值活动可能转人工,不能默认放行后再期待事后修复。
安全维度还影响数据模型。request_id 要绑定用户和活动,防止其他用户复用;运营修改容量需要权限与审计;报名导出要保护个人信息;日志不能把完整身份凭证写入。限流指标不能直接把每个 user ID 当标签,否则监控系统会被高基数拖垮。
十二、可观测性应当回答设计假设是否成立
CPU、内存和总 QPS 只能说明系统在运行,不能回答用户为什么报名失败。监控要沿前面的推导链布置:
- 业务层:报名确认率、已满拒绝率、重复请求率、处理中数量与年龄、确认数和容量是否满足不变量;
- 接口层:按活动类型和结果码统计的到达 QPS、成功率、p50/p95/p99 延迟;
- 并发层:在途请求、线程池、连接池、队列长度、等待时间和主动拒绝量;
- 数据层:条件更新成功率、唯一键冲突、事务延迟、锁等待、复制延迟和缓存命中率;
- 异步层:生产与消费速率、积压条数、最老消息年龄、重复消费和死信;
- 资源层:CPU、内存、GC、网络、磁盘延迟与使用率。
OpenTelemetry 将 traces、metrics 和 logs视为从不同角度描述系统活动的信号。一次报名应携带 trace ID、request ID 和 registration ID:trace 展示时间花在哪一跳,指标判断问题是否普遍,结构化日志用于追查具体状态转移。订单号和用户 ID 适合放在可检索日志字段中,不适合直接成为无界指标标签。
告警应对应能执行的动作。缓存命中率下降但数据库仍有充足余量,不一定需要立刻叫醒值班人员;报名确认率突然下降、处理中记录持续老化、数据库锁等待超过预算,则可能需要限流、切换降级路径或停止活动。面向 SLO 和业务不变量的告警比“CPU 超过 80%”更接近真实风险。
十三、压测要验证拐点、失败方式和恢复过程
正常流量下成功只是最低条件。高并发系统需要知道吞吐在什么位置停止增长,p99 从哪里开始陡升,哪种资源先饱和,以及过载后能否通过拒绝和降级保持核心路径。AWS 的弹性设计建议把负载测试列为适应需求变化的实践之一;Google SRE 也指出很多服务接近过载时会进入非线性区域,无法只靠理论预测。
报名系统至少需要以下实验:
- 逐级增加均匀流量,得到吞吐、p99 与资源使用率曲线。
- 在十秒内注入目标尖峰,验证缓存预热、自动扩容和入口限流是否及时。
- 让 80% 请求集中到一个活动,确认热点行、缓存 key 和消息分区的上限。
- 注入数据库延迟,观察在途并发、超时、重试和处理中记录是否保持有界。
- 暂停消息消费者后恢复,验证积压年龄、扩容速度与恢复洪峰。
- 在事务提交前后杀死实例,检查同一 request ID 是否得到唯一、可查询的结论。
- 发布期间下线部分实例,确认剩余容量仍满足内部 SLO。
压测流量必须接近真实分布。全是轻量查询会高估容量,全是同一个请求又可能低估缓存效果。测试数据量也要接近生产,否则索引完全驻留内存、表没有历史版本和后台任务,结果只能代表一个干净的实验环境。
每次测试要保存代码版本、配置、数据规模、流量模型和资源规格。结论应该写成“在某个版本和 p99 目标下,每实例可承载多少某类请求”,而不是永久有效的“服务容量为 5 万 QPS”。Google SRE 的容量规划说明同样强调通过定期负载测试把原始资源映射到服务容量,因为软件效率和依赖会随版本变化。
十四、架构评审要检查决策链,而不是组件数量
一份可以执行的系统设计文档至少应交付以下内容:
| 产物 | 要回答的问题 |
|---|---|
| 范围与假设 | 设计覆盖哪些用户动作,哪些需求暂不处理 |
| SLO 与不变量 | 什么叫成功,哪些错误不可接受 |
| 容量模型 | 峰值怎样产生,各层放大倍数和冗余是多少 |
| 数据模型 | 权威状态在哪里,唯一约束和状态机怎样维护事实 |
| API 契约 | 成功、明确失败、处理中和未知结果怎样表达 |
| 读写路径 | 每一步访问什么状态,提交点在哪里 |
| 故障矩阵 | 依赖超时、宕机、延迟和重复时怎样处理 |
| 扩展与分片 | 瓶颈出现后按哪个维度水平扩展 |
| 可观测性 | 哪些指标能证伪容量与可靠性假设 |
| 验证计划 | 压测、故障注入、恢复演练和验收阈值是什么 |
评审时可以从任意组件反向追问。为什么需要 Redis?因为活动详情读多写少、允许短时间陈旧,并且数据库读取在目标峰值下超出预算。Redis 挂了怎么办?限速回源或返回带版本的旧值。怎样证明方案有效?预热后做热点突发测试,并观察回源 QPS 和数据库 p99。三个问题都答不上来时,这个框只是架构图里的装饰。
反过来也要避免为了图简单而漏掉约束。只有“客户端、服务、数据库”三个框并不天然错误,只要单库压测能达到容量,事务能维护不变量,故障恢复满足目标,并且团队能运维。架构复杂度来自已经证明的需求,不来自题目里出现了“高并发”三个字。
十五、用一份简短模板完成下一次系统设计
面对新的系统设计题,可以按下面的顺序推进:
1. 范围:列出核心用户动作、非目标和外部依赖
2. 承诺:写出 SLO、不变量、RPO/RTO 与安全边界
3. 规模:估算峰值 QPS、并发、数据量、带宽和热点分布
4. 状态:设计主键、唯一约束、状态机与权威数据源
5. 基线:先给出能够正确工作的最小架构
6. 路径:走完一次读取、一次写入和一次结果未知
7. 演进:按已知瓶颈加入缓存、队列、分片与冗余
8. 故障:为每个远程边界定义超时、重试、幂等和降级
9. 观测:让指标、日志和 trace 对应前面的假设
10. 验证:用负载、热点、故障和恢复实验给出验收标准
这份顺序不会替你选择数据库或消息队列,但它能暴露选择所需的信息。短链接系统会把重点放在编码空间、重定向读取和热点 key;秒杀会把重点放在库存、排队与防超卖;支付会把重点放在结果未知、状态机、补偿与对账。题目不同,推导方法保持一致。
系统设计是否完成,可以由另一个工程师能否根据文档回答下列问题来衡量:系统承诺什么,峰值从哪里来,权威状态在哪里,一次请求在哪个点得到确定结论,依赖失败后怎样收敛,以及通过什么实验判断方案合格。画完所有框并不能代替这些答案。后续文章会逐项深入容量评估、性能压测、缓存、消息队列、热点治理和高可用,再把这些机制装回具体系统。
参考资料
如果这篇文章对你有帮助