全景:留言下單的一筆訂單,會經過哪些系統

· tech

#war-story#live-commerce#system-design

📑 目錄

這是一個新系列,也是這個部落格第一個戰爭故事。我在前公司實際做過一個直播代購電商平台——使用者看直播、在留言區打字下單,後面接著庫存、金流、物流一整條鏈。這系列不是回憶錄:我想帶著現在的功力(DDIA、Redis、Kafka、SRE 都重新讀過一輪之後)把當年的仗重打一次——每章從一個真實需求出發,先講當年怎麼做、踩了什麼,再給重來版的設計。第一篇先把全景鋪開。

這門生意:讓聊天室變成收銀機

規則其實很簡單:

  • 主播開直播賣代購商品,講到一件商品就喊出它的 key(例如「2601」)。
  • 想買的人在留言區打 2601+2:key + 數量,這樣就是下單 2 件。
  • 同一個人對同一個 key 重複留言,以最後一筆為準——打了 2601+2 又改打 2601+1,就是買 1 件。
  • 留言成立的單直接進購物車,而且佔庫存;商品有庫存上限,不能超賣
  • 購物車分兩種:直播下單的佔庫存,自己在電商平台上加入的不佔;數量都可以隨時調整。
  • 留言來源不只一個平台:FB、IG、自建直播間都有,規則要一體適用。
  • 最麻煩的一條:留言下單的人,可能根本還沒註冊帳號——但庫存還是要先卡給他。

每一條規則單看都是一個週末的工作量。難的是把它們放在同一句話裡:三千人同時留言,搶 20 件庫存,一半的人沒帳號,留言來自三個平台——這才是這個系統真實的樣子。

一筆訂單的旅程

先看一筆訂單從留言到出貨的完整路徑,這條路徑就是整個系列的目錄:

留言進來 FB / IG / 自建 統一留言事件 每源一個 adapter 解析 key+n 同人重複取最後一筆 身分 identity 沒帳號也要能掛單 佔庫存購物車 庫存 −n・不能超賣 結帳 兩種購物車合併 第三方金流 webhook・冪等 出貨前處理 合併出貨・交物流 逾期未付 → 釋放庫存 每一站都是系列的一章:留言接入、身分、庫存、購物車、金流、出貨前處理
一筆訂單的旅程:上排把「一則留言」變成「一筆掛在身分上的單」,下排把「佔到的庫存」變成「送到家的貨」。

每一站展開都有自己的取捨,細節留給各章;這裡只點出每站「真正的問題是什麼」:

  • 留言進來:三個平台的取得方式完全不同(webhook、輪詢、自家直推),先各自收斂成統一的留言事件,下游才不用管來源。「重複留言取最後一筆」看似簡單,其實是 LWW——你得先回答「以什麼順序為準」。
  • 身分:留言的是 fb:12345,不是我們的會員。庫存要卡給這個身分,帳號之後才出現、再把單認領回去。這是全系列最容易被低估的一章。
  • 佔庫存:不能超賣是這個系統唯一的鐵律,本質是不變量在併發下的保衛戰。佔了就要有釋放——逾期、取消、改數量,缺一個就會有庫存慢性失血。
  • 結帳與金流:兩種購物車在這裡合流;金流的 webhook 會重複、會亂序、會根本不來,不可靠網路的每一課這裡都會考。
  • 出貨前處理:代購的特色——多場直播的單合併出貨,等貨到齊、一人多單併一箱,揀貨那一刻庫存帳才真正落地。

真正難的三件事

功能列表不難,難的是三個橫貫整條鏈的性質:

  1. 尖峰不是曲線,是牆。 主播喊完 key 的那三秒,幾千則留言同時湧入——這不是一般電商的「大促流量爬升」,是一句話觸發的 thundering herd(開賣尖峰那章專門講)。
  2. 不能超賣,而且是在併發下不能。 庫存 20 件、三千人搶,任何「先查再扣」的天真寫法都會賣超。這條不變量必須在架構層保證,不能靠小心。
  3. 三本帳要對齊。 庫存帳、訂單帳、金流帳分屬三個系統,跑久了一定歪;歪了之後你要能回答「以誰為準」——沒有事實來源的系統,對帳只能用猜的(對帳那章專門講)。

重來版的總架構:先寫下事實,再派生一切

當年的系統其實已經摸到這個門口:留言抓進來會先清洗、落地進 DB,再由一個 job 批次消費。重來,我只做一件事——把這個當年只當成「緩衝」的東西,扶正成整個架構的中心:留言不是待處理的輸入,是要永久留下的事實:

FB 留言 IG 留言 自建直播間 adapter × N:每源一個,收斂成統一留言事件(M×N → M+N) 留言/訂單事件流(append-only・唯一事實來源) 下單解析 key+n・LWW 身分 identity→account 庫存預留 不能超賣 購物車・訂單 狀態機 金流・出貨 冪等介接 三本帳對帳:庫存帳・訂單帳・金流帳(派生視圖,定期對齊)
重來版架構:事件流是唯一的事實來源,每個服務用自己的節奏消費;三本帳都是派生視圖,歪了以事件流為準。

這個架構的三個主張,也是整個系列反覆出現的主旋律:

  1. 先寫下事實,再談業務。 留言一進來就 append 到事件流,業務邏輯是事件流的消費者。這一步同時解掉三件事:尖峰(寫入只是 append,削峰交給 queue,Kafka 就是為此而生)、除錯(所有輸入都留痕,可以重放)、對帳(有了「以誰為準」的答案)。這正是 DDIA 第三部分「寫快的事實來源+讀快的派生視圖」的實戰版。
  2. 庫存是預留模型,預留主是身分不是帳號。 「佔庫存」其實是 reservation:卡給 fb:12345 這個 identity,帳號註冊後再認領。身分先於帳號,是這個業務跟一般電商最不一樣的地方。
  3. 所有外部介接,預設對方會重複、會亂序、會消失。 平台 webhook、金流回調、物流狀態——冪等不是加分項,是第一天就要有的地基。

系列地圖

系列照一筆訂單的旅程排,再加上維運與演進的縱深(章節還在長,以最新的系列列表為準):

  • 開場與地基:全景(本篇)、技術棧與 CI/CD 起手式。
  • 交易主線:留言即下單、身分與帳號、庫存、購物車與訂單。
  • 錢與貨:第三方金流、出貨前處理。
  • 營運面:主播後台、權限、優惠券與金額、通知、風控與黑名單。
  • 橫切與維運:開賣尖峰、沒有 SRE 的上線維運、三本帳對帳。
  • 演進與終局:monolith 拆微服務、如果變成 SaaS、小團隊的 EM 視角,以及最後的「Re:如果真的重來」。

反思

當年最大的錯,不是技術選型

是把「落地」當成緩衝,而不是事實來源。我們其實做對了一半:留言抓進來會先清洗、寫進 DB,再批次消費——已經是半個 event log。但清洗完原文就丟了,清洗規則漏接的留言永遠追不回來;批次處理失敗的留言直接略過,沒有留痕、沒有補救路徑,那位顧客就無聲消失。這是當年刻意的取捨:為了讓主播看到最快的庫存狀態,寧可掉單。而快其實也沒守住——尖峰時消費跟不上,留言到下單成功可以差到幾分鐘,主播那頭的庫存認知跟系統對不上,客訴就是從那幾分鐘裡長出來的。重來我仍然會選快——但快可以用「晚點處理」換,不能用「丟掉事實」換:事實還在,晚到的單補得回來、客訴查得到人;事實丟了,連道歉都不知道要跟誰道。

不變量比功能重要

功能寫錯,改掉重上就好;超賣是把不存在的貨賣掉,要一個一個跟客人道歉退款,信任賠進去就回不來。所以這個系統裡「不能超賣」「錢帳對齊」「介接冪等」這三條,我重來會在第一天就寫進設計文件的第一頁——不是因為優雅,是因為這三條的違約成本是用商譽付的。這也是我寫這系列想傳達的核心:電商系統的難,不在功能,在不變量。

為什麼用「重來」的形式寫

因為「如果重來你會怎麼設計」是我自己面試別人最愛問的問題,而我發現最誠實的回答方式,是拿一個自己真的做過、真的做錯過的系統來答。接下來每一章都會有兩個聲部:當年怎麼做、為什麼那樣做;重來怎麼做、又為什麼。有趣的從來不是標準答案,是中間那段差距——那才是這幾年真正學到的東西。