从一个需求到一个系统:全栈工程师的架构设计实战——订单系统的从零推演

发布时间:2026/9/29 11:53:33
从一个需求到一个系统:全栈工程师的架构设计实战——订单系统的从零推演
摘要前面三篇分别讲了系统的运转请求生命周期、排障深夜告警和预测容量规划这篇讲更前置的能力拿到一个新需求怎么从一张白纸推演出一套合理的架构。以给一家中型电商从零设计订单系统为主线完整走一遍架构设计的思考路径先问业务需求分析、再问规模容量估算、再定边界服务拆分、再选存储数据建模、再设计核心流程下单链路、最后设计非功能需求幂等/超时/降级。每一步给问什么问题、有哪些选项、怎么选、选错了会怎样。这不是一篇微服务架构最佳实践而是一篇决策过程的完整记录——架构的价值不在画出来的图而在图背后的每一次取舍。建议收藏。目录架构设计的误区从画图开始是错的第一步问业务——需求里藏着 80% 的架构决策第二步问规模——数字决定形态第三步定边界——单体还是微服务一张决策表第四步选存储——订单数据的三种形态与三种库第五步设计核心流程——下单链路的每一步取舍第六步非功能设计——幂等、超时、降级三板斧架构评审怎么让别人接受你的设计总结架构是长出来的不是设计出来的1. 架构设计的误区从画图开始是错的先说一个普遍的误区拿到需求打开画图工具开始画微服务框图——用户服务、订单服务、支付服务中间加个消息队列再配个网关。图很漂亮但这个顺序是反的。架构图是架构设计的输出不是输入。在画出第一张框图之前你至少要回答三个问题业务到底要什么——不是做一个订单系统而是订单创建的高峰在哪、退款率和支付成功率是多少、超时未支付的订单怎么处理规模到底多大——日订单 1 万和日订单 500 万是两个完全不同的系统团队多大多强——5 人团队上 12 个微服务是自杀式架构。架构设计的正确顺序先问业务再问规模再定边界再选存储再画图。画图永远是最后一步。下面以一个具体需求为主线走完这个流程。需求原文甲方的原话通常就这么短我们要做一个线上商城能下单、能支付、能退款。就这一句话。接下来的每一节都是从这句话里榨出架构决策的过程。2. 第一步问业务——需求里藏着 80% 的架构决策能下单、能支付、能退款九个字藏着至少十个必须问清的业务问题。每一个问题的答案都直接改变架构必须问的问题为什么影响架构假设的答案订单高峰在什么时候决定是否要削峰、限流晚 8~10 点双峰峰值系数 6%有没有秒杀/抢购场景秒杀和普通下单是两套系统一期没有二期可能有 ⚠️超时未支付的订单怎么办需要订单超时关闭机制决定是否引入延迟消息30 分钟未支付自动取消退款流程有多复杂仅退款 vs 退货退款状态机复杂度差 3 倍退货退款需审核支付渠道有几家多渠道适配层的设计微信支付宝两家库存是否要防超卖决定库存扣减方案下节展开必须防超卖硬要求订单要保留多久归档策略、冷热分层3 年合规要求发票/对账要不要对账是独立子系统要日终对账移动端还是 WebAPI 设计、认证方式都要一套 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团队 5QPS 5000团队 5~15QPS 万级团队 15部署复杂度极低低高需要 K8s 全家桶故障隔离无一挂全挂部分好独立扩容无读服务可独立扩好开发效率最高高低联调/发版协调适合本例边缘✅ 正确答案过度设计本例的答案轻度拆分3 个服务。拆分依据不是领域驱动设计的标准拆法而是两个实际理由3 服务的拆法按变化速率和资源特征拆不按教科书领域拆┌────────────────────────────────────────────────┐│ ① 商城核心服务单体 ││ 商品浏览 购物车 订单创建 ││ 理由三者共享数据模型、一起发布、写压力小 │├────────────────────────────────────────────────┤│ ② 支付服务独立 ││ 支付渠道适配 回调处理 对账 ││ 理由安全敏感独立部署/审计、渠道频繁变化、 ││ 回调是异步流量可与主站独立扩容 │├────────────────────────────────────────────────┤│ ③ 消费者服务异步 ││ Kafka 消费者集合短信/积分/风控/数据分析 ││ 理由纯异步流量可独立扩容、独立故障 ││ 消费堆积不影响主链路 │└────────────────────────────────────────────────┘拆分的两条原则按变化速率拆变化快的支付渠道和变化慢的订单核心分开让它们独立发布、互不阻塞。微服务的真正收益是发布独立性不是技术先进性。按资源特征拆同步流量用户在等和异步流量系统后台处理分开——消费者服务堆积不影响下单主链路这是故障隔离的实际价值。什么时候该进一步拆让监控告诉你当某个模块的发布频率明显拖累其他模块想发订单必须连商品一起发或者某个模块的资源需求和整体错位消费者想扩容但商品服务被迫跟着扩再拆不迟。架构是长出来的先用轻架构扛住让真实的痛点告诉你下一刀切哪里。5. 第四步选存储——订单数据的三种形态与三种库订单数据有三种访问形态对应三种存储数据形态访问特征存储选型为什么热数据3 个月按 user_id/order_id 精确查、状态流转更新PG 单库预留分表事务强一致写 QPS 低用户订单列表按 user_id 时间倒序分页PG 索引 缓存索引足够不需要 ES商家/运营查询按时间/状态/商品多维度组合查ES异步同步多维组合查询是 PG 索引的弱项一个刻意的设计决策订单列表不用 ES商品搜索才用。用户查自己的订单只有一种模式我的订单、按时间倒序一个复合索引(user_id, created_at desc)完全覆盖把订单列表塞进 ES 是典型的为技术找场景。ES 只服务商家多维度查订单这一个真正的弱项场景且从 Kafka 异步同步允许分钟级延迟。存储设计的两条原则一个数据一种主存储PG 是订单的 source of truthES 是派生的查询视图可随时重建。派生视图坏了可以重建主存储绝不能坏——这个主从关系必须在架构里显式声明否则会出现两边都当主的数据一致性灾难。分表方案现在设计、以后实施订单表按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 → refundedtotal_amount BIGINT NOT NULL, -- 金额用整数分BIGINT绝不用浮点pay_amount BIGINT NOT NULL,pay_channel TEXT, -- wechat / alipaypay_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 statuspending_payWHERE status pending_pay; -- 部分索引只为超时扫描服务三个建模细节每个都对应一类真实事故金额用整数分BIGINT绝不用浮点浮点数的 0.10.2 ≠ 0.3订单金额出现 1 分钱误差就是对账灾难。金额的一切运算都在整数域。每个状态流转留时间戳paid_at/shipped_at/done_at不只是记录是对账与争议仲裁的唯一依据——用户说付了但系统没发货靠的就是这些时间戳和渠道回调日志。部分索引为特定查询服务超时扫描只关心pending_pay状态的订单部分索引让这个扫描从全表索引扫描变成只有几百行的索引——索引是为查询模式服务的不是为表服务的。感觉以后可能要查的索引不要建每个索引都是写入的税。数据建模的本质把业务规则状态机、金额精度、幂等键直接翻译成表结构和约束——凡是数据库层面能约束的NOT NULL、UNIQUE、状态条件更新都不要留给应用层自觉。数据库是最后防线应用层代码会被人改错约束不会。6. 第五步设计核心流程——下单链路的每一步取舍核心链路是架构的心脏。下单链路有四个必须设计的环节每个环节都有取舍6.1 环节一库存扣减——防超卖的三种方案防超卖是本需求的硬约束。三种主流方案方案原理优点缺点适合数据库行锁乐观扣减UPDATE stock SET nn-1 WHERE sku? AND n1强一致、实现极简热点行锁竞争QPS 上限 ~500本例正确答案Redis 预扣 异步落库Redis 原子扣减异步写 DBQPS 极高一致性复杂对账补偿秒杀场景队列串行化扣减请求全部排队串行处理绝对不超卖吞吐受限于单消费者超高价值商品本例选方案一理由来自规模画像写峰值 850 QPS大促单行锁扣减在 PG 上实测可扛 500~800 QPS够用。方案二的 Redis 预扣是秒杀的答案一期用它是杀鸡用牛刀还多养了一致性难题。但架构上预留升级路径库存扣减独立成模块接口隔离二期秒杀只替换实现不改调用方。-- 乐观扣减一条 SQL 天然防超卖affected_rows 0 即售罄UPDATE inventory SET stock stock - 1WHERE 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 timeoutWHERE status pending_payAND 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 statuspaid, paid_atnow()WHERE id$1 AND statuspending_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. 架构评审怎么让别人接受你的设计架构文档写完只是开始评审通过才算数。四条实战建议先讲决策再讲图。评审会上前 10 分钟讲三个决策为什么轻度拆分、为什么不用 ES 存订单列表、为什么定时扫描代替延迟消息——决策被接受了图怎么画都没人反对决策没被接受图画得再美也会被推翻。每个决策配为什么不选另一个。评审的质疑永远是为什么不用 X——为什么不用 Redis 扣库存为什么不用消息队列做超时关闭。方案的 Strength 没有说服力替代方案的 Weakness 才有说服力。每个决策备好另一个选项 它在本场景的具体问题。给架构留被推翻的余地。明确说出什么情况下这个设计要改——如果日订单过百万订单表物理分库如果上了秒杀库存扣减换 Redis 方案。主动暴露边界比被别人戳穿体面得多也说明你真的想过。写两页纸摘要。完整文档 30 页没人看评审前发两页纸规模画像8 个数字 3 个核心决策 遗留风险。架构沟通的效率决定架构落地的效率。9. 总结架构是长出来的不是设计出来的架构设计的完整流程回顾一句话需求→ 问业务10 个问题藏着 80% 决策→ 问规模8 个数字数字决定形态→ 定边界3 个服务按变化速率和资源特征拆→ 选存储三种形态三种库一种数据一种主存储→ 核心流程四个环节每环节有取舍→ 非功能幂等/超时/降级三板斧→ 画图最后一步六句话收束全文架构图是输出不是输入——画图之前先问业务、算规模顺序反了就是空中楼阁。数字决定形态写 850 QPS 的系统不需要分库分表读写比 30:1 的系统先优化读路径。微服务的真正收益是发布独立性——按变化速率拆不按教科书领域拆5 人团队 3 个服务封顶。一个数据一种主存储——PG 是事实源ES 是派生视图派生视图可重建主存储不能坏。中间件是负债不是资产——定时扫描换掉延迟消息省下的不是钱是一辈子的运维责任。架构是长出来的——先用轻架构扛住一期让真实的痛点而不是技术理想告诉你下一刀切哪里。从零设计一个系统最难的不是技术选型而是在信息不全的情况下做出可辩护的决策——每个决策都经得起为什么不用 X的追问。至此全栈系列四篇《一次点击背后》怎么运转→《深夜告警之后》怎么排障→《在告警响起之前》怎么预测→ 本篇怎么从零设计。想看秒杀系统或账务系统的专场推演评论区点单。参考与延伸阅读《Domain-Driven Design》——限界上下文但记住5 人团队不需要 10 个限界上下文Google SRE Book: Designing for Reliability《Designing Data-Intensive Applications》——存储选型的原理根基AWS Well-Architected Framework——非功能设计的清单化