跳到正文

电商架构图:商品目录、购物车、结账与订单

一套适合中型商店的真实电商架构:各服务拥有哪些数据、必须保持一致的结账路径,以及之后的订单事件。

· 创始人兼开发者

发布于

架构图。顾客先到达 CDN 和 API 网关,网关路由到 Catalog、Search、Cart、Checkout 和 Orders。Checkout 调用 Inventory 和支付服务商。Orders 向 order-events 主题发布事件,由 Fulfillment、Notifications 和 Analytics 读取。

9:00,促销开始。CDN 撑住了商品页,但结账出了问题:两位顾客买下了同一件夹克的最后一件,超时后的一次重试在同一张卡上留下两笔支付冻结,而仓库因为一个事件丢失,晚了一小时才得知一笔订单。

这些都不是容量问题,而是边界问题:哪些数据归谁所有,以及哪些步骤必须在顾客等待期间完成。

本文介绍一套适合中型商店的真实电商架构:组件与数据归属、结账路径的逐步流程、之后的订单流水线、峰值流量,以及平台的架构图。

从顾客的行为出发

五条用户旅程涵盖了商店的大部分行为,它们的要求差别很大:

旅程 路径上的组件 哪些数据可以过期
浏览 CDN、网关、Catalog 缓存页面上的价格和库存,可过期数分钟
搜索 搜索索引 变更的商品,可过期数秒
购物车 Cart 顾客刚修改的内容不可过期
结账 Checkout、Inventory、支付服务商、Orders 不可过期
订单追踪 Orders、Fulfillment 物流状态,可过期数分钟

浏览和搜索承载了大部分请求,能容忍数据过期;结账承载的请求很少,却一点也不能容忍。架构的大部分设计都源于这一划分。

各组件及其拥有的数据

在边缘,CDN 缓存页面和图片,API 网关(或每种客户端各一个 backend for frontend)负责认证、限流并路由其余请求。网关之后的每个服务都拥有自己的数据:

服务 拥有的数据 典型存储 一致性要求
Catalog 商品、规格、价格 Postgres,经缓存读取 最终一致;结账时重新读取价格
Search 带分面的商品索引 OpenSearch 或 Elasticsearch 最终一致,随目录变更更新
Cart 每位顾客的购物车条目 带 TTL 的 Redis 每位顾客读己之写
Checkout 会话:合计金额、预留与支付 ID Postgres 会话内强一致
Inventory 每个 SKU 的库存、预留 Postgres 强一致:绝不超卖
Orders 订单、状态历史、outbox Postgres 强一致:每个订单只有一个写入方
支付服务提供商(PSP) 授权、扣款、退款 PSP 自身 以 PSP 的记录为准
Fulfillment 发货单、运单号 WMS 或 3PL 最终一致,由事件驱动

其他服务使用所有者的 API 或事件,绝不直接访问它的表。库存和订单需要事务;购物车和搜索索引可以滞后,甚至丢失一次写入,也不会造成实际损害。

结账路径的逐步流程

结账是唯一一个顾客需要同时等待多个服务的流程:

  1. 重新计算购物车价格。 Checkout 读取购物车,从目录获取当前价格,并在服务端计算税费、运费和折扣。永远不要相信浏览器传来的合计金额。

  2. 带过期时间预留库存,这里是 15 分钟:

    -- One transaction: take the stock, then record the hold
    UPDATE stock SET reserved = reserved + 2
     WHERE sku = 'JKT-042-M' AND on_hand - reserved >= 2;  -- 0 rows: out of stock, stop
    INSERT INTO reservations (checkout_id, sku, qty, expires_at)
    VALUES ('chk_41a', 'JKT-042-M', 2, now() + interval '15 minutes');
    

    两位顾客抢最后一件夹克时,PostgreSQL 默认的 Read Committed 隔离级别会让第二个 update 等待,并针对新行重新检查它的 WHERE 条件,因此其中一人会得到 0 行。清理任务会把过期的预留归还库存。使用托管支付页面时,要让两边的时钟对齐:Stripe Checkout 会话默认持续 24 小时,除非你设置更短的过期时间(最少 30 分钟)或自行让它过期;checkout.session.expired 会通知你释放商品。

  3. 用幂等键创建支付。 Checkout 向 PSP 请求一笔服务端合计金额的支付,采用手动扣款,并在调用前保存幂等键。超时后用同一个键重试:Stripe 会返回已保存的响应,而不是创建第二笔支付。它的键最长 255 个字符,24 小时后可能被清除。

  4. 授权。 先延长库存预留,避免它在支付中途失效。顾客在 PSP 的输入框中填写卡信息,所以卡号不会到达你的服务器,发卡行还可能要求 3-D Secure。成功后资金被冻结:采用手动扣款的 Stripe 支付会进入 requires_capture 状态,并发出 payment_intent.amount_capturable_updated。要信任这个 webhook,而不是浏览器的重定向;参见支付网关集成图。

  5. 下单。 Orders 创建状态为 placed 的订单,并在同一事务中把 OrderPlaced 写入 outbox 表。随后 Checkout 通知 Inventory 把预留转为分配。

  6. 发货时扣款。 Fulfillment 发货后,Orders 执行扣款。截至撰写时(2026 年 11 月),Stripe 文档指出,线上银行卡授权通常有效 7 天;商户发起的 Visa 交易为 5 天(准确地说是 4 天 18 小时);之后资金释放,支付被取消。大多数银行卡支付只允许扣款一次,部分扣款会释放其余金额。

如果在订单生成之前某个步骤失败,就按相反顺序撤销前面的步骤:取消授权,释放预留。这就是一个以 Checkout 为编排者的 Saga。

资金、库存与订单状态在哪里变化

订单状态 设置时机 支付 库存
结账中 库存已预留 已创建,未授权 预留 15 分钟
Placed PSP 通知授权成功 资金已冻结 已分配给订单
Shipped Fulfillment 发货 已扣款 从现有库存中扣除
Canceled 发货前取消 授权已取消 已释放
Returned 收到退货 已退款 检验后重新入库

难处理的情况都在各行之间。如果授权在预留过期之后才到达,库存还有的话就重新预留;否则取消授权并告知顾客。如果缺货预订(backorder)超过 7 天的期限,就在首次发货时扣收全款,或在 PSP 提供时使用延长授权(据 Stripe 称,某些卡组织最长 30 天),或者先收款,再退还无法发出的部分。有些支付方式,如 ACH 和 iDEAL,根本无法单独扣款。

订单生成之后

placed 之后的一切都异步运行。中继进程把 OrderPlaced 从 outbox 发布到 order-events 主题,这样在提交和消息代理之间不会丢事件(参见 transactional outbox 模式)。Fulfillment 拣货、打包并发货,然后发布 OrderShipped,触发扣款。Notifications 发送邮件;Analytics 把每个事件加载进数据仓库。

投递保证至少一次,所以消费者要按事件 ID 去重。以订单 ID 作为主题的键,可以让每个订单的事件保持顺序。

微服务架构图则是在 OrderPlaced 之后才向银行卡扣款。这适合不需要顾客参与的支付;3-D Secure 需要顾客在场,所以这套设计在结账中完成授权。

扛住峰值流量

假设促销一小时内有 7,200 笔订单,转化率 2%,每次会话 10 次页面浏览:共 360,000 次会话和 360 万次页面浏览,也就是每秒约 1,000 次页面浏览,对应每秒 2 笔订单。两部分各需要自己的保护:

  • 在边缘缓存商品目录。 使用 Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=60,CDN 会把商品页提供 5 分钟,之后在后台重新获取的同时再多提供最多 1 分钟,这正是 RFC 5861 的定义。命中率 90% 时,源站每秒只看到 100 次页面浏览,而不是 1,000 次。价格变动时清除缓存;结账本来也会重新核对。
  • 在结账前排队。 虚拟等候室按 Inventory 和 PSP 能承受的速率放行顾客,就像票务预订系统设计中那样。
  • 保护热门 SKU。 同一个 SKU 的预留只更新一行,而且一次只能处理一个。每个事务持有锁 2 ms 的话,这一行的上限约为每秒 500 次预留。保持事务简短且不含 PSP 调用,把特价爆款的库存拆到多行,并限制每笔订单的数量。
  • 提前了解 PSP 的速率限制,在促销开始之前。

比这更简单地起步

很多商店不该把这些全部自己搭建。Shopify 或 BigCommerce 这样的托管平台会替你运行目录、购物车和结账;这时你的图展示的是围绕它的应用和 webhook。

自建时,往往最好从模块化单体起步:同样的边界,作为一个可部署单元内的模块。2019 年,Shopify 的工程师介绍了他们如何围绕订单、配送、库存和计费重新组织最大的 Ruby on Rails 代码库之一,而不是把它拆成微服务。当某个模块需要独立扩展或独立发布时,再把它拆成服务;这张图依然适用。

电商平台架构图

架构图。顾客先到达 CDN 和 API 网关,网关路由到 Catalog、Search、Cart、Checkout 和 Orders。Checkout 调用 Inventory 和支付服务商。Orders 向 order-events 主题发布事件,由 Fulfillment、Notifications 和 Analytics 读取。

19 个组件 · 17 个连接

在 Archboard 中打开
图表内容
  • Shopper
  • CDN
  • API gateway / BFF
  • Catalog
  • Search
  • Cart
  • Checkout
  • Orders
  • Inventory
  • Payment provider (PSP)
  • order-events
  • Fulfillment
  • Notifications
  • Analytics warehouse
  • HTTPS
  • misses, API
  • GET /products
  • GET /search
  • POST /cart/items
  • POST /checkout
  • GET /orders/{id}
  • product changes
  • read cart
  • reserve, 15 min
  • authorize + idempotency key
  • place order
  • capture on OrderShipped
  • OrderPlaced
  • placed, shipped
  • all events
中型网店背后的各项服务。浏览流量走缓存,数据可能略有过期;结账在顾客等待期间预留库存、授权银行卡并下单;此后的一切都靠订单事件驱动。

这张图的读法如下:

  • 请求从左上角进入:顾客、CDN,然后是网关,网关把每条旅程路由到一个服务。
  • 蓝色是浏览路径。 虚线边表示搜索索引追上商品变更。
  • 琥珀色是结账路径,顾客在这里等待:读取购物车、预留库存、授权并下单。
  • 支付服务商会收到两次调用:结账时授权,以及 OrderShipped 时扣款(青色虚线)。
  • 紫色虚线边是事件,从 outbox 经 order-events 流向 Fulfillment、Notifications 和 Analytics。
  • 每个服务都标明自己的存储,Markdown 块列出具体数字。

把电商架构图画清楚

  • 在网关的边上标注路由,如 GET /products,让读者能把页面对应到服务。
  • 在每个服务上标明存储。 共享数据库不应该被忽略。
  • 实线表示等待,虚线表示稍后。 顾客等待的调用应该与事件看起来不同。
  • 在边上写明时间。 预留时长和扣款触发点最需要评审。
  • 把授权和扣款画成两条边。 只有一个指向 PSP 的箭头,会掩盖关键的时间点。
  • 不要画: Pod、区域、税务和反欺诈供应商,以及后台管理。

在 Archboard 中构建

  1. 用 Google 或 GitHub 登录并创建一个图表。
  2. 从 C4 Model 包添加一个人员,再从其下方的云包添加一个 CDN 和一个 API 网关。
  3. 从 C4 Model 包按一列添加五个容器节点:Catalog、Search、Cart、Checkout 和 Orders,每个的描述中写明它的存储。
  4. 添加一个用于 Inventory 的容器、一个用于支付服务商的外部系统,以及一个来自数据包、用于 order-events 的主题。在它们右侧,为 Fulfillment 和 Notifications 添加容器节点,再从数据包添加一个数据仓库。
  5. 用带路由标签的连线把网关连到这五个服务。结账调用用实线,商品变更、扣款和事件用虚线。
  6. 从基础包添加一个写有具体数字的 Markdown 块,再添加文本便签用作图例和关键规则。

如果你的服务通过 Compose 文件运行,Docker Compose 转图表工具可以生成方框草稿;微服务架构模板是另一个起点。这个示例共用 15 个图表节点,在 Free 套餐的限制之内。

常见问题

电商架构的主要组件有哪些? CDN、API 网关、目录、搜索、购物车、结账、库存和订单等服务、支付服务商,以及 Fulfillment、Notifications 和 Analytics。托管平台把其中大部分打包在一起。

网店应该使用微服务吗? 默认不需要。边界相同的模块化单体更容易运维,Shopify 的核心系统就选择了它。当某个模块需要独立扩展或独立发布时,再把它拆成服务。

如何防止库存超卖? 对每个 SKU 用一次原子的条件更新来预留库存,给每次预留设置过期时间,并且只在订单生成时才分配。永远不要根据缓存页面来判断是否有货。

网店应该在什么时候扣款? 对实物商品,通常在发货时扣款,这样只对实际发出的商品收费。线上银行卡授权通常有效 7 天。在 Stripe 上,Klarna 允许 28 天,PayPal 为 10 天,Stripe 会尝试把它延长到 20 天。

电商网站如何应对流量高峰? 它们从 CDN 提供商品目录,在结账前让顾客排队,并保护热门 SKU 和支付服务商。要把结账容量与页面浏览量分开规划。

免费开始 — 1 个席位,无需信用卡。

相关文章

← 返回博客

一分钟打开画布

使用 Google 或 GitHub 登录。单席位免费,团队加入后再升级。