☰
从一个需求到一个系统:全栈工程师的架构设计实战——订单系统的从零推演
2026/9/29 11:52:09 网站建设 项目流程

摘要:前面三篇分别讲了系统的运转(请求生命周期)、排障(深夜告警)和预测(容量规划),这篇讲更前置的能力:拿到一个新需求,怎么从一张白纸推演出一套合理的架构。以"给一家中型电商从零设计订单系统"为主线,完整走一遍架构设计的思考路径:先问业务(需求分析)、再问规模(容量估算)、再定边界(服务拆分)、再选存储(数据建模)、再设计核心流程(下单链路)、最后设计非功能需求(幂等/超时/降级)。每一步给"问什么问题、有哪些选项、怎么选、选错了会怎样"。这不是一篇"微服务架构最佳实践",而是一篇决策过程的完整记录——架构的价值不在画出来的图,而在图背后的每一次取舍。建议收藏。

目录

  1. 架构设计的误区:从"画图"开始是错的
  2. 第一步:问业务——需求里藏着 80% 的架构决策
  3. 第二步:问规模——数字决定形态
  4. 第三步:定边界——单体还是微服务,一张决策表
  5. 第四步:选存储——订单数据的三种形态与三种库
  6. 第五步:设计核心流程——下单链路的每一步取舍
  7. 第六步:非功能设计——幂等、超时、降级三板斧
  8. 架构评审:怎么让别人接受你的设计
  9. 总结:架构是长出来的,不是设计出来的

1. 架构设计的误区:从"画图"开始是错的

先说一个普遍的误区:拿到需求,打开画图工具,开始画微服务框图——用户服务、订单服务、支付服务,中间加个消息队列,再配个网关。图很漂亮,但这个顺序是反的。

架构图是架构设计的输出,不是输入。在画出第一张框图之前,你至少要回答三个问题:

  1. 业务到底要什么——不是"做一个订单系统",而是"订单创建的高峰在哪、退款率和支付成功率是多少、超时未支付的订单怎么处理";
  2. 规模到底多大——日订单 1 万和日订单 500 万,是两个完全不同的系统;
  3. 团队多大多强——5 人团队上 12 个微服务,是自杀式架构。

架构设计的正确顺序:先问业务,再问规模,再定边界,再选存储,再画图。画图永远是最后一步。下面以一个具体需求为主线走完这个流程。

需求原文(甲方的原话通常就这么短):

"我们要做一个线上商城,能下单、能支付、能退款。"

就这一句话。接下来的每一节,都是从这句话里"榨"出架构决策的过程。

2. 第一步:问业务——需求里藏着 80% 的架构决策

"能下单、能支付、能退款"九个字,藏着至少十个必须问清的业务问题。每一个问题的答案都直接改变架构:

必须问的问题

为什么影响架构

假设的答案

订单高峰在什么时候?

决定是否要削峰、限流

晚 8~10 点双峰,峰值系数 6%

有没有秒杀/抢购场景?

秒杀和普通下单是两套系统

一期没有,二期可能有 ⚠️

超时未支付的订单怎么办?

需要"订单超时关闭"机制,决定是否引入延迟消息

30 分钟未支付自动取消

退款流程有多复杂?

仅退款 vs 退货退款,状态机复杂度差 3 倍

退货退款,需审核

支付渠道有几家?

多渠道适配层的设计

微信+支付宝两家

库存是否要防超卖?

决定库存扣减方案(下节展开)

必须防超卖(硬要求)

订单要保留多久?

归档策略、冷热分层

3 年合规要求

发票/对账要不要?

对账是独立子系统

要,日终对账

移动端还是 Web?

API 设计、认证方式

都要,一套 API

一期什么时候上线?

时间决定范围,范围决定架构

3 个月 ⚠️

最后两个问题最重要,也最常被忽略:

"有没有秒杀"这个问题的答案,价值等于一周的架构工作量。秒杀场景的架构(内存预扣库存、令牌桶排队、异步落单)和普通下单完全是两套系统。一期没有就按普通下单设计,但架构要预留秒杀的扩展点(库存扣减层独立出来),否则二期加秒杀就是重写。

"3 个月上线"这个约束比所有技术问题加起来都重。3 个月、5 人团队,意味着:单体或轻度拆分(3~4 个服务封顶)、不引入复杂中间件、先把主链路跑通。架构是被时间和团队规模约束出来的,不是被技术理想约束出来的。

3. 第二步:问规模——数字决定形态

业务问清后,用容量模型(上一篇的方法)算出系统的规模画像:

规模画像(从业务数字推导):

预计日订单 20 万单
→ 峰值下单 QPS = 200,000 × 6% / 7200s(双峰) ≈ 170 QPS
→ 大促 5 倍 ≈ 850 QPS
→ 商品浏览峰值 ≈ 下单 × 30 = 5000 QPS(读多写少!)

关键数字:
· 写 QPS(下单):峰值 170,大促 850 ← 写压力很小!
· 读 QPS(浏览):峰值 5000,大促 25000 ← 读压力是写的 30 倍
· 订单表年增量:20 万/天 × 365 ≈ 7300 万/年 ← 单表 2 年内到亿级
· 一期团队:5 人 ← 人力是最稀缺资源

这四个数字直接决定后面所有技术选型:

  • 读写比 30:1→ 读路径优化(缓存、读写分离)优先级远高于写路径;写架构可以简单(单库),读架构必须设计(缓存层)。
  • 写峰值 850 QPS→ 一个 PG 主库绰绰有余(单库写 3000+ QPS 无压力),分库分表一期完全不需要。
  • 订单表年增 7300 万→ 2 年内单表过亿,要在表设计时就预留分表方案(按 user_id 分片),但一期不实施。
  • 5 人团队→ 3~4 个服务封顶,绝对不上"每个领域一个微服务"的教科书架构。

规模画像的本质:把"我们要做一个大系统"翻译成 8 个以内可以验证的数字。数字不到某个量级,就不要用那个量级的架构。

4. 第三步:定边界——单体还是微服务,一张决策表

现在可以回答"单体还是微服务"了。这不是信仰问题,是决策问题:

维度

单体

轻度拆分(3~4 服务)

完整微服务(10+)

适合规模

QPS < 500,团队 < 5

QPS < 5000,团队 5~15

QPS 万级,团队 15+

部署复杂度

极低

低

高(需要 K8s 全家桶)

故障隔离

无(一挂全挂)

部分

好

独立扩容

无

读服务可独立扩

好

开发效率

最高

高

低(联调/发版协调)

适合本例?

边缘

✅ 正确答案

过度设计

本例的答案:轻度拆分,3 个服务。拆分依据不是"领域驱动设计的标准拆法",而是两个实际理由:

3 服务的拆法(按"变化速率"和"资源特征"拆,不按教科书领域拆):

┌────────────────────────────────────────────────┐
│ ① 商城核心服务(单体): │
│ 商品浏览 + 购物车 + 订单创建 │
│ 理由:三者共享数据模型、一起发布、写压力小 │
├────────────────────────────────────────────────┤
│ ② 支付服务(独立): │
│ 支付渠道适配 + 回调处理 + 对账 │
│ 理由:安全敏感(独立部署/审计)、渠道频繁变化、 │
│ 回调是异步流量(可与主站独立扩容) │
├────────────────────────────────────────────────┤
│ ③ 消费者服务(异步): │
│ Kafka 消费者集合:短信/积分/风控/数据分析 │
│ 理由:纯异步流量,可独立扩容、独立故障, │
│ 消费堆积不影响主链路 │
└────────────────────────────────────────────────┘

拆分的两条原则:

  1. 按"变化速率"拆:变化快的(支付渠道)和变化慢的(订单核心)分开,让它们独立发布、互不阻塞。微服务的真正收益是发布独立性,不是技术先进性。
  2. 按"资源特征"拆:同步流量(用户在等)和异步流量(系统后台处理)分开——消费者服务堆积不影响下单主链路,这是故障隔离的实际价值。

什么时候该进一步拆?让监控告诉你:当某个模块的发布频率明显拖累其他模块(想发订单必须连商品一起发),或者某个模块的资源需求和整体错位(消费者想扩容但商品服务被迫跟着扩),再拆不迟。架构是长出来的:先用轻架构扛住,让真实的痛点告诉你下一刀切哪里。

5. 第四步:选存储——订单数据的三种形态与三种库

订单数据有三种访问形态,对应三种存储:

数据形态

访问特征

存储选型

为什么

热数据(3 个月)

按 user_id/order_id 精确查、状态流转更新

PG 单库(预留分表)

事务强一致,写 QPS 低

用户订单列表

按 user_id + 时间倒序分页

PG 索引 + 缓存

索引足够,不需要 ES

商家/运营查询

按时间/状态/商品多维度组合查

ES(异步同步)

多维组合查询是 PG 索引的弱项

一个刻意的设计决策:订单列表不用 ES,商品搜索才用。用户查自己的订单只有一种模式(我的订单、按时间倒序),一个复合索引(user_id, created_at desc)完全覆盖;把订单列表塞进 ES 是典型的"为技术找场景"。ES 只服务"商家多维度查订单"这一个真正的弱项场景,且从 Kafka 异步同步(允许分钟级延迟)。

存储设计的两条原则:

  1. 一个数据一种主存储:PG 是订单的 source of truth,ES 是派生的查询视图(可随时重建)。派生视图坏了可以重建,主存储绝不能坏——这个"主从关系"必须在架构里显式声明,否则会出现"两边都当主"的数据一致性灾难。
  2. 分表方案现在设计、以后实施:订单表按user_id哈希分 16 张表(orders_00~orders_15),一期逻辑分表(还在一个库里,但表名带分片),2 年后数据量逼线时物理分库,应用层无感迁移。分表是"现在设计、以后实施",分库是"数据说了算",都不是"现在就上"。

5.1 订单表的建模:状态机即表结构

存储选型之后是具体的表建模。订单表的建模核心是把状态机直接翻译进表结构——状态字段、流转时间戳、以及"每一步谁做的":

CREATE TABLE orders_00 (
id BIGSERIAL,
order_no TEXT UNIQUE NOT NULL, -- 业务订单号(对外展示)
user_id BIGINT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending_pay',
-- pending_pay → paid → shipped → done
-- ↘ cancelled
-- paid → refunding → refunded
total_amount BIGINT NOT NULL, -- 金额用整数分(BIGINT),绝不用浮点
pay_amount BIGINT NOT NULL,
pay_channel TEXT, -- wechat / alipay
pay_trade_no TEXT, -- 渠道交易号(幂等+对账)
created_at TIMESTAMPTZ DEFAULT now(),
paid_at TIMESTAMPTZ, -- 每个状态流转都留时间戳
shipped_at TIMESTAMPTZ,
done_at TIMESTAMPTZ,
close_reason TEXT, -- cancelled/timeout 的原因
updated_at TIMESTAMPTZ DEFAULT now()
);
-- 索引:按"实际查询模式"建,不按"感觉可能查"建
CREATE INDEX ON orders_00 (user_id, created_at DESC); -- 用户订单列表(主力查询)
CREATE INDEX ON orders_00 (status, created_at) -- 超时扫描(WHERE status='pending_pay')
WHERE status = 'pending_pay'; -- 部分索引:只为超时扫描服务

三个建模细节,每个都对应一类真实事故:

  1. 金额用整数分(BIGINT),绝不用浮点:浮点数的 0.1+0.2 ≠ 0.3,订单金额出现 1 分钱误差就是对账灾难。金额的一切运算都在整数域。
  2. 每个状态流转留时间戳:paid_at/shipped_at/done_at不只是记录,是对账与争议仲裁的唯一依据——"用户说付了但系统没发货",靠的就是这些时间戳和渠道回调日志。
  3. 部分索引为特定查询服务:超时扫描只关心pending_pay状态的订单,部分索引让这个扫描从全表索引扫描变成只有几百行的索引——索引是为查询模式服务的,不是为表服务的。感觉"以后可能要查"的索引不要建:每个索引都是写入的税。

数据建模的本质:把业务规则(状态机、金额精度、幂等键)直接翻译成表结构和约束——凡是数据库层面能约束的(NOT NULL、UNIQUE、状态条件更新),都不要留给应用层自觉。数据库是最后防线,应用层代码会被人改错,约束不会。

6. 第五步:设计核心流程——下单链路的每一步取舍

核心链路是架构的心脏。下单链路有四个必须设计的环节,每个环节都有取舍:

6.1 环节一:库存扣减——防超卖的三种方案

"防超卖"是本需求的硬约束。三种主流方案:

方案

原理

优点

缺点

适合

数据库行锁(乐观扣减)

UPDATE stock SET n=n-1 WHERE sku=? AND n>=1

强一致、实现极简

热点行锁竞争,QPS 上限 ~500

本例正确答案

Redis 预扣 + 异步落库

Redis 原子扣减,异步写 DB

QPS 极高

一致性复杂(对账补偿)

秒杀场景

队列串行化

扣减请求全部排队串行处理

绝对不超卖

吞吐受限于单消费者

超高价值商品

本例选方案一,理由来自规模画像:写峰值 850 QPS(大促),单行锁扣减在 PG 上实测可扛 500~800 QPS,够用。方案二的 Redis 预扣是秒杀的答案,一期用它是杀鸡用牛刀还多养了一致性难题。但架构上预留升级路径:库存扣减独立成模块(接口隔离),二期秒杀只替换实现不改调用方。

-- 乐观扣减:一条 SQL 天然防超卖(affected_rows = 0 即售罄)
UPDATE inventory SET stock = stock - 1
WHERE sku_id = $1 AND stock >= 1;

6.2 环节二:订单创建——同步与异步的切分线

下单接口里,哪些步骤同步做、哪些异步做?切分线只有一条:用户当下需要知道结果的,同步;其余全部异步。

下单接口(同步,目标 < 300ms) 异步(Kafka 事件驱动)
┌─────────────────────┐ ┌──────────────────────┐
│ 1. 参数校验 │ │ 发送短信通知 │
│ 2. 库存乐观扣减 ✅ │ ┌─────┐ │ 积分发放 │
│ 3. 创建订单(事务)✅ │──→│Kafka│──→│ 风控扫描 │
│ 4. 生成支付单 ✅ │ └─────┘ │ 数据分析 │
│ 5. 投递事件 (1ms) │ │ 商家 ES 同步 │
└─────────────────────┘ │ 超时关闭定时检查 │
↑ 用户等的就是"下单成功" └──────────────────────┘
↑ 失败可重试,不影响用户

切分线的两个推论:

  • 库存扣减必须同步(用户要知道"有没有抢到"),但它要轻(一条 UPDATE);
  • 积分、短信必须异步(用户不需要下单瞬间收到积分),且消费者幂等(重复消费不重复发积分,幂等键 = 订单号 + 事件类型)。

6.3 环节三:订单超时关闭——延迟消息的两种实现

"30 分钟未支付自动取消"看似简单,工程上两种方案:

方案

原理

优点

缺点

延迟消息

RabbitMQ 死信/Kafka 定时轮询,30 分钟后投递

实时、精确

引入新中间件

定时扫描

每 5 分钟扫"超时未支付"的订单批量关闭

零新组件、天然幂等

最多延迟 5 分钟关闭

本例选定时扫描:5 分钟的关闭延迟在业务上完全可接受(库存占用多 5 分钟而已),换来零新中间件。技术选型的原则:中间件是负债不是资产——每引入一个新组件,你都要养它一辈子(部署、监控、升级、排障),除非收益明确大于这份负债。

-- 定时关闭:天然幂等(状态条件保证重复执行安全)
UPDATE orders SET status = 'closed', close_reason = 'timeout'
WHERE status = 'pending_pay'
AND created_at < now() - interval '30 minutes';

6.4 环节四:支付回调——钱相关的一切都要幂等

支付回调是整个系统唯一不能出错的地方(多发货或少发货都是资损)。三条铁律:

# payment/callback.py —— 支付回调处理(骨架)
async def handle_pay_callback(order_id: str, amount: int, channel: str,
channel_trade_no: str, sign: str):
# 铁律一:先验签,后处理(伪造回调 = 直接资损)
if not verify_sign(sign, order_id, amount, channel_trade_no):
raise HTTPException(400, "invalid signature")

# 铁律二:幂等优先——按渠道交易号幂等,重复回调直接返回成功
# (支付渠道会重发回调,不幂等 = 重复发货)
inserted = await db.execute(
"""INSERT INTO pay_callbacks(channel_trade_no, ...) VALUES (...)
ON CONFLICT (channel_trade_no) DO NOTHING""")
if inserted == 0:
return "SUCCESS" # 已处理过,直接确认(渠道要求返回成功防重发)

# 铁律三:金额校验——回调金额 ≠ 订单金额,一律挂起人工介入
order = await db.fetchrow("SELECT pay_amount FROM orders WHERE id=$1", order_id)
if amount != order["pay_amount"]:
await alert_ops(f"金额不一致: {order_id} {amount} != {order['pay_amount']}")
return "SUCCESS" # 先确认防重发,人工介入处理

# 状态机流转:只允许 pending_pay → paid,其他状态流转拒绝(防乱序回调)
updated = await db.execute(
"""UPDATE orders SET status='paid', paid_at=now()
WHERE id=$1 AND status='pending_pay'""")
if updated == 0:
await alert_ops(f"状态流转异常: {order_id}") # 可能是乱序/重复回调
await producer.send("order-events", {"type": "paid", "order_id": order_id})
return "SUCCESS"

三条铁律之外还有一个隐性设计:状态机显式建模。订单状态(pending_pay → paid → shipped → done,加 cancelled/refunding 分支)是严格的有向图,所有状态流转都走WHERE status = 期望前态的条件更新——条件更新让乱序回调、重复回调天然安全,比"先查后改"的写法安全一个量级。

7. 第六步:非功能设计——幂等、超时、降级三板斧

核心流程之外,三个非功能设计必须一期就做,它们不是"优化项",是"正确性的一部分":

幂等:所有写接口都要设计幂等键。下单接口幂等键 = 客户端生成的请求 ID(前端防重复提交),支付回调幂等键 = 渠道交易号(渠道防重发)。幂等键的选择标准:同一个业务动作,不管发生几次,幂等键必须相同。

超时:所有跨服务调用必须显式设置超时,且上游超时 > 下游超时之和(否则上游先超时了,下游还在白干)。下单链路:网关 5s > 下单接口 3s > 库存 1s + DB 0.5s。没有超时的调用等于"愿意为它等到天荒地老"。

降级:每个非核心依赖都要有降级方案,且降级方案要演练过。下单链路的降级矩阵:

依赖

挂了会怎样

降级方案

库存服务

不能下单 ❌ 核心依赖,不降级

扩容 + 熔断排队

推荐服务

详情页缺一块

降级:返回默认位图,页面正常

积分服务

用户没积分

异步重试 + 死信队列,无感

ES 查询

商家查不了订单

降级:回退 PG 简单查询

降级矩阵的价值在"事前共识":大促当天,"推荐服务要不要降级"不应该是现场争论的问题,而是预案里写好的答案。

8. 架构评审:怎么让别人接受你的设计

架构文档写完只是开始,评审通过才算数。四条实战建议:

  1. 先讲决策,再讲图。评审会上前 10 分钟讲三个决策(为什么轻度拆分、为什么不用 ES 存订单列表、为什么定时扫描代替延迟消息)——决策被接受了,图怎么画都没人反对;决策没被接受,图画得再美也会被推翻。
  2. 每个决策配"为什么不选另一个"。评审的质疑永远是"为什么不用 X"——"为什么不用 Redis 扣库存""为什么不用消息队列做超时关闭"。方案的 Strength 没有说服力,替代方案的 Weakness 才有说服力。每个决策备好"另一个选项 + 它在本场景的具体问题"。
  3. 给架构留"被推翻的余地"。明确说出"什么情况下这个设计要改"——"如果日订单过百万,订单表物理分库""如果上了秒杀,库存扣减换 Redis 方案"。主动暴露边界比被别人戳穿体面得多,也说明你真的想过。
  4. 写"两页纸摘要"。完整文档 30 页没人看,评审前发两页纸:规模画像(8 个数字)+ 3 个核心决策 + 遗留风险。架构沟通的效率决定架构落地的效率。

9. 总结:架构是长出来的,不是设计出来的

架构设计的完整流程回顾:

一句话需求
→ 问业务(10 个问题,藏着 80% 决策)
→ 问规模(8 个数字,数字决定形态)
→ 定边界(3 个服务,按变化速率和资源特征拆)
→ 选存储(三种形态三种库,一种数据一种主存储)
→ 核心流程(四个环节,每环节有取舍)
→ 非功能(幂等/超时/降级三板斧)
→ 画图(最后一步)

六句话收束全文:

  1. 架构图是输出不是输入——画图之前先问业务、算规模,顺序反了就是空中楼阁。
  2. 数字决定形态:写 850 QPS 的系统不需要分库分表,读写比 30:1 的系统先优化读路径。
  3. 微服务的真正收益是发布独立性——按变化速率拆,不按教科书领域拆;5 人团队 3 个服务封顶。
  4. 一个数据一种主存储——PG 是事实源,ES 是派生视图;派生视图可重建,主存储不能坏。
  5. 中间件是负债不是资产——定时扫描换掉延迟消息,省下的不是钱,是一辈子的运维责任。
  6. 架构是长出来的——先用轻架构扛住一期,让真实的痛点(而不是技术理想)告诉你下一刀切哪里。

从零设计一个系统,最难的不是技术选型,而是在信息不全的情况下做出可辩护的决策——每个决策都经得起"为什么不用 X"的追问。至此全栈系列四篇:《一次点击背后》(怎么运转)→《深夜告警之后》(怎么排障)→《在告警响起之前》(怎么预测)→ 本篇(怎么从零设计)。想看"秒杀系统"或"账务系统"的专场推演,评论区点单。

参考与延伸阅读:

  • 《Domain-Driven Design》——限界上下文(但记住:5 人团队不需要 10 个限界上下文)
  • Google SRE Book: Designing for Reliability
  • 《Designing Data-Intensive Applications》——存储选型的原理根基
  • AWS Well-Architected Framework——非功能设计的清单化

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询