System Design·2026年5月8日·11 分鐘閱讀

活動網站搶購系統:用 Go 與 Redis 扛住瞬間高併發

L
Louis Wu

後端工程師,做過多檔大型活動/搶購網站,專注於高併發與限量資源的正確扣減。

活動網站最刺激的時刻,就是整點開搶的那一秒。平常每秒幾十個請求的系統,在開搶瞬間可能衝到每秒上萬。而且搶購有個比一般流量更難的點:庫存是有限的,而且絕對不能超賣。送出 100 個名額,結果賣出 101 個,就是事故。這篇講我怎麼用 Go 加 Redis 扛住這種場景。

第一件事:別讓流量打到資料庫

最直覺的寫法是「查庫存 → 夠就扣 → 寫訂單」,全部打 DB。這在開搶瞬間會瞬間把資料庫連線池打爆,然後整站一起陪葬。

正確的心態是:把「擋掉」和「成交」分開。絕大多數請求其實是搶不到的,你要做的是用最便宜的方式把它們快速擋在外層,只讓少量可能成交的請求進到核心。

我的分層大致是:

  • 最外層:CDN/靜態化頁面,純看的人根本不該打到後端
  • 接著:限流與「資格」檢查,擋掉重複點擊、機器人
  • 庫存判斷:在 Redis,不在 DB
  • 真正的下單與扣款:非同步,排隊處理

用 Redis 做原子扣庫存

庫存扣減的核心要求是原子性:判斷「還有沒有」和「扣一個」必須是不可分割的一步,否則併發下必超賣。

在 Redis 裡,我用 Lua script 把「檢查庫存 > 0 就遞減」包成一個原子操作。Redis 是單執行緒執行命令的,一段 Lua script 在執行期間不會被其他命令插隊,這正好就是我們要的原子性。

要點:

  • 庫存值預先載入 Redis(預熱),別等開搶才從 DB 拉
  • 扣減回傳剩餘量,小於 0 代表搶完,直接回「售罄」
  • 同一個使用者的重複請求要先擋(用 Redis 的 SETNX 之類做去重),避免一個人扣多個

這樣絕大部分的「搶」都在 Redis 的記憶體裡解決,單機就能扛數萬 QPS,DB 完全不參與這一步。

削峰:用佇列把尖峰攤平

Redis 判定「搶到」之後,我不會當場同步去寫訂單、扣款——那又會把壓力導回 DB。而是把「搶到」這個事件丟進訊息佇列,後端用固定數量的 worker 依自己的步調消費,慢慢寫訂單。

使用者端的體驗是:點下去馬上得到「搶購成功,訂單處理中」,實際的訂單在背景幾秒內建立完成。這就是削峰填谷:把一秒鐘湧入的尖峰,攤平成幾十秒的平穩寫入,DB 從頭到尾都在舒適區。

一致性:Redis 扣了,DB 一定要補上

這裡有個必須面對的問題:Redis 扣成功了,但後續寫 DB 的 worker 掛了怎麼辦?那就會「庫存扣了但訂單沒建」,等於少賣。

處理方式:

  • Redis 扣減和「搶到」事件要可靠地進到佇列(別 fire-and-forget)
  • Worker 消費要冪等,同一個搶購事件重放不會建出兩張單
  • 收尾要對帳:活動結束後比對 Redis 扣掉的數量 vs DB 實際成立的訂單數,有落差要能查、能補

我的原則是:寧可少賣、不可超賣。所以設計上讓「Redis 扣減」作為超賣的最終防線,DB 端再加一道唯一約束(同一使用者同一活動只能一張單)當保險。

別忘了「搶不到的人」

工程上很容易只關注成交路徑,但搶購系統 99% 的請求是失敗的。這些請求的體驗一樣重要:

  • 售罄要快速、明確回應,不要讓人轉圈圈
  • 真正的瓶頸常常不是扣庫存,而是搶不到的人不斷重整、重試帶來的二次流量
  • 前端要做好「已售罄」的狀態鎖定,減少無效請求

小結

搶購系統的設計哲學就是一句話:用便宜的資源擋掉大多數,用昂貴的資源服務少數

  • 靜態化與限流擋在最外層
  • 庫存判斷用 Redis + Lua 原子扣減,DB 不參與熱路徑
  • 訊息佇列削峰,把尖峰攤平成平穩寫入
  • 全鏈路冪等,事後對帳,寧少賣不超賣

做過幾次活動之後我才真正體會到:高併發系統的功力,不在於讓系統「跑得多快」,而在於讓系統在最極端的那一秒,該擋的擋得住、該對的一分不差

#高併發#秒殺#Redis#Go#限流