mybid
即時競標 · 11 分鐘閱讀 · 18 次閱讀

即時競標系統的網站架構:WebSocket 技術解析

深入解析即時競標系統的技術架構,從 WebSocket 連線到併發出價處理的完整技術方案

快速解答: 即時競標系統的核心是 WebSocket 長連線,讓伺服器能主動推送最新出價給所有瀏覽同一拍品的買家。搭配原子鎖處理併發、Append-Only 的出價記錄設計,就能做到毫秒級即時更新且不遺漏任何一筆出價。

即時競標系統的網站架構:WebSocket 技術解析

你有沒有在拍賣網站上,看著最後 10 秒倒數,手指放在出價按鈕上等待,結果按下去之後頁面轉了好幾秒才顯示結果——然後告訴你「已被超越」?這種體驗就是因為背後用的是傳統 HTTP 輪詢,而不是 WebSocket。

我花了不少時間研究各家拍賣平台的技術實作,從補破網的 GraphQL Subscriptions 到國際大站的架構設計都看過。今天就把即時競標系統的關鍵技術拆解給你,不管你是要自己架站還是跟工程團隊溝通,看完都會更有底。

WebSocket 即時競標系統架構總覽

WebSocket 到底是什麼?為什麼拍賣需要它?

WebSocket 是一種讓瀏覽器和伺服器之間建立持久雙向通道的通訊協定,跟傳統的 HTTP 請求-回應模式完全不同。

用生活化的比喻來說:HTTP 就像寄信,你寄一封(請求)、對方回一封(回應),每次都要重新寫地址貼郵票。WebSocket 就像打電話,接通之後雙方可以隨時講話,不用每句話都重新撥號。

傳統 HTTP 輪詢的問題

早期的拍賣網站用的是「短輪詢」——瀏覽器每隔 1-3 秒就問一次伺服器「有新出價嗎?」。這有幾個嚴重問題:

  1. 延遲太高:如果設定每 2 秒問一次,你可能最多晚 2 秒才知道有人出價。在結標前的激烈時刻,2 秒就是天差地別
  2. 伺服器壓力大:假設一個拍品有 200 人在看,每人每 2 秒發一次請求,光一個拍品伺服器每秒就要處理 100 個無效請求(大部分時候沒有新出價)
  3. 流量浪費:90% 以上的請求回來都是「沒有新資料」,完全是浪費頻寬

WebSocket 怎麼解決這些問題

WebSocket 連線建立後,伺服器可以主動在有新出價的瞬間就推送給所有連線中的用戶端。延遲通常可以控制在 50-200 毫秒,而且只在有資料時才傳輸,沒事就靜靜待著不佔頻寬。

實測數據:一個有 500 人同時觀看的熱門拍品,用 HTTP 輪詢每秒要處理 250 個請求;改用 WebSocket 後,只有在真正有人出價時才會產生一次廣播,伺服器負載降低了 95% 以上。

競標系統的核心架構長什麼樣?

一套完整的即時競標系統至少包含四個核心模組:連線管理、出價處理、廣播推送、防狙擊機制。 下面一個一個拆解。

競標系統四大核心模組

連線管理:Channel 的設計

在 WebSocket 的世界裡,「Channel」是一個很重要的概念。你可以把它想像成聊天室——每個拍品就是一個 Channel,只有正在瀏覽這個拍品的人會加入。

以 Laravel Reverb(一個原生的 WebSocket 伺服器)為例,Channel 通常設計成這樣:

  • 公開 Channelauction.{auctionId} — 任何人都能加入,接收出價更新
  • 私有 Channelprivate-bidder.{userId} — 只有本人能加入,接收個人通知(被超越、得標等)

連線認證很關鍵。私有 Channel 必須驗證用戶身分,確保 A 買家收不到 B 買家的專屬通知。這通常透過 HTTP 端點做 token 驗證,確認後才允許加入。

出價處理:原子鎖搞定併發

拍賣最怕什麼?兩個人幾乎同時出價,結果系統搞混了。

想像這個場景:目前最高價 1,000 元,A 和 B 在相差 3 毫秒內都按下「出價 1,100 元」。如果沒有適當的併發控制,系統可能會:

  1. 同時讀到最高價是 1,000 元
  2. 兩邊都判斷 1,100 > 1,000,合法
  3. 同時寫入兩筆 1,100 元的出價
  4. 搞不清楚誰才是合法的出價者

解法是用 原子鎖(Atomic Lock)。以 Redis 的 Cache::lock() 為例,處理邏輯是:

1. 取得鎖 → lock("bid:{auctionId}", 5秒)
2. 讀取當前最高價
3. 驗證出價是否合法(>= 最高價 + 最低加價幅度)
4. 寫入出價記錄
5. 釋放鎖

鎖的持有時間通常設 3-5 秒,如果超時沒釋放就自動過期,避免死鎖。整個流程在 10-30 毫秒內完成,用戶幾乎無感。

出價記錄:Append-Only 設計

這是一個很聰明的設計決策——出價表(bids table)只新增、不更新、不刪除。

為什麼?因為拍賣涉及金錢和法律責任,每一筆出價都是一份承諾。如果允許修改或刪除記錄,出了糾紛根本查不清楚。Append-Only 設計讓每筆出價都有完整的時間戳和金額記錄,形成不可篡改的出價歷程。

資料表大概長這樣:

欄位 說明
id 唯一編號
auction_id 拍品編號
user_id 出價者
amount 出價金額(以「分」儲存)
created_at 出價時間

注意金額用「分」為單位儲存(integer),不用 decimal。1,000 元就存 100000。這樣做是為了避免浮點數運算的精度問題——你不會想在拍賣裡看到什麼 1000.000001 元的鬼東西。

防狙擊機制怎麼做?

「狙擊」是拍賣界最惹人厭的策略之一——在最後幾秒突然出價,讓其他買家來不及反應。 好的拍賣系統一定要有防狙擊機制。

防狙擊延長機制時序圖

目前業界最常見的做法是「條件式延長」:

結標前 3 分鐘內,如果有 2 個(含)以上不同買家出價,就自動延長結標時間 5 分鐘。

這個規則看起來簡單,實作起來有幾個細節要注意:

  1. 計數器的準確性:要精確統計「最後 3 分鐘內有幾個不同用戶出過價」,不能只看出價次數,因為同一個人連續加價不算
  2. 延長後的重新計算:延長 5 分鐘後,新的「最後 3 分鐘」起算點也要跟著變。如果延長後又有人狙擊,就再延長一次,理論上可以無限延長
  3. 前端倒數器的同步:延長結標時間後,所有連線中的用戶端都要即時更新倒數計時器。這就是 WebSocket 的用武之地——伺服器推一個 auction.extended 事件,前端收到就重設計時器

以補破網拍賣為例,他們的防狙擊延長規則就是上述的「3 分鐘 + 2 人 + 延長 5 分鐘」,實測效果不錯,既保護一般買家權益,又不會把拍賣時間拖得太長。

廣播推送:讓每個人都看到最新狀態

WebSocket 廣播的重點在於「選擇性推送」——不是什麼資料都推給所有人。 這直接影響系統效能和用戶體驗。

一筆新出價進來後,系統需要推送的事件大概有這些:

事件類型 推送對象 內容
bid.placed 該拍品所有觀看者 新出價金額、出價者暱稱、出價時間
bid.outbid 被超越的前最高出價者 你的出價已被超越,目前最高價
auction.extended 該拍品所有觀看者 新的結標時間
auction.ended 該拍品所有觀看者 + 得標者 最終成交價、得標者

注意「被超越通知」要送到私有 Channel,只有那個被超越的人收得到。如果推到公開 Channel,大家都知道誰被超越了,這涉及隱私。

為什麼要用 ShouldBroadcastNow

在 Laravel 的架構裡,事件廣播預設會丟到 Queue(佇列)排隊處理。一般場景沒問題,但拍賣不行——Queue 可能有其他任務在排隊(發 Email、處理圖片等),等輪到你的出價事件時可能已經過了好幾秒。

ShouldBroadcastNow 跳過 Queue 直接廣播,確保延遲在毫秒等級。這個設計取捨很值得學:關鍵路徑上不要排隊,非關鍵路徑才用 Queue。 出價廣播是關鍵路徑,得標後的 Email 通知就可以排隊慢慢來。

技術選型怎麼挑?

市面上做 WebSocket 的技術方案不少,我整理幾個跟拍賣系統相關的:

方案 適合場景 優勢 考量
Laravel Reverb Laravel 生態系 原生整合、設定簡單、不需外部服務 相對新,社群資源還在累積
Pusher / Ably 不想自架 WebSocket 伺服器 全託管、穩定、有 SLA 按連線數和訊息數收費
Socket.io + Node.js 前端團隊熟 Node 生態豐富、彈性大 需自己搞部署和擴展
GraphQL Subscriptions 已用 GraphQL 的專案 跟現有 API 一致 學習曲線、技術複雜度較高

如果你是 Laravel 技術棧,我真心建議直接用 Reverb。不用額外裝服務、不用額外付費、跟 Livewire 的整合也很滑順。用 Livewire 的 #[On('echo-private:...')] 裝飾器就能直接監聽 WebSocket 事件,前端幾乎不用寫 JavaScript。

響應式設計和行動裝置優化有興趣的話,WebSocket 在行動端的處理又是另一個話題了,特別是行動網路的斷線重連機制。

壓力測試:你的系統撐得住嗎?

做即時競標系統不能只管功能做出來就好,你一定要知道系統的極限在哪裡。我見過不少拍賣平台在大場次(同時上百件結標)時直接掛掉,就是因為沒做過壓力測試。

幾個關鍵指標要測:

  • 同時連線數:你的 WebSocket 伺服器能撐多少條同時連線?Reverb 預設單台大概能撐 10,000-20,000 條
  • 出價吞吐量:每秒能處理多少筆出價?原子鎖是瓶頸,單一拍品大約能做到每秒 30-50 筆
  • 廣播延遲:從出價寫入到所有用戶端收到更新,P99 延遲要低於 500 毫秒
  • 斷線重連:模擬網路不穩,確認用戶端能自動重連且不會漏接出價

如果你預期熱門拍品會有超過 1,000 人同時觀看,建議做水平擴展——部署多台 WebSocket 伺服器,用 Redis Pub/Sub 做節點間的訊息同步。

壓力測試與效能指標

常見問題 FAQ

Q:WebSocket 連線會一直佔著伺服器資源嗎? 會,但佔的很少。一條 idle 的 WebSocket 連線大概只佔 2-5 KB 記憶體。1 萬條連線也就 20-50 MB,對現代伺服器來說不算什麼。真正吃資源的是頻繁的訊息廣播,不是連線本身。

Q:用戶關掉瀏覽器再打開,會漏接出價嗎? 設計得好的話不會。用戶重新連線後,前端應該主動發一個 HTTP 請求拉取最新的拍品狀態(包含當前最高價、剩餘時間等),然後再接上 WebSocket 繼續聽後續更新。這叫「初始狀態 + 增量更新」模式。

Q:手機上的 WebSocket 穩定嗎? 比桌機差一點,主要是行動網路切換(4G/5G/WiFi 切換時)可能斷線。解法是前端實作 heartbeat 檢測 + 自動重連機制,通常每 30 秒送一次 ping,如果 3 次沒回應就判定斷線並嘗試重連。

Q:不用 WebSocket,用 SSE(Server-Sent Events)行不行? 可以用來做「伺服器推送出價更新」這一段,但 SSE 是單向的——只能伺服器推給客戶端,客戶端沒辦法透過 SSE 送出出價。所以你還是需要搭配 HTTP API 來處理出價動作。如果只需要「看」不需要「出價」的場景(例如圍觀頁面),SSE 確實夠用而且更簡單。

相關文章

團標是什麼?多人合資競標的策略與風險

團標就是找朋友一起出錢競標,分攤大件拍品的成本。但費用怎麼分、誰來出價、出事了怎麼辦?這篇把團標的眉角一次講清楚。

· 9 分鐘 · 2 次閱讀
閱讀 →

即時競標系統怎麼運作:技術原理白話解說

好奇拍賣平台怎麼做到即時更新出價?這篇用白話解釋 WebSocket、原子鎖、防狙擊延長等即時競標的核心技術,讓你了解系統背後的運作邏輯。

· 8 分鐘 · 5 次閱讀
閱讀 →

競標預算怎麼抓:避免越標越高的理性出價法

競標最怕越標越高停不下來,學會設定預算上限和分配策略,讓你享受競標樂趣又不傷荷包。

· 9 分鐘 · 9 次閱讀
閱讀 →