用 Laravel 打造拍賣系統:架構決策與最容易低估的三件事
一句話總結:拍賣系統表面上是電商加一個倒數計時器,實際上難的是即時性、併發控制與結標的準時觸發這三件事,而這三件事在一般電商專案裡幾乎都不會遇到,也因此最常被低估。
「就是購物車再加個倒數吧?大概兩個月?」
我聽過不只一個人這樣估拍賣系統的工期。這個誤判很好理解——從畫面上看,拍賣頁面確實就是商品頁多了個計時器和出價按鈕。
但真正做下去會發現,麻煩的東西全在看不見的地方:兩個人在同一毫秒出價要怎麼處理、結標時間到了誰負責關掉它、有一百個人同時盯著同一件商品時價格怎麼即時更新到每個人的螢幕上。這些問題在一般電商裡不存在,因為電商沒有「同一件商品同時只能有一個贏家」這種設定。
這篇把用 Laravel 做拍賣系統會碰到的關鍵決策整理出來。不談程式碼細節,談的是那些選錯了要重寫的地方。
拍賣系統為什麼比一般電商難做?
難點不在功能數量,而在三個一般電商完全不需要處理的特性:強即時性、強一致性、以及時間本身就是業務邏輯。
強即時性:電商的商品價格可以快取十分鐘沒人會抱怨。拍賣不行——你看到的價格如果慢了三秒,你的出價就是無效的,而且使用者會覺得系統在騙他。這代表整條讀取路徑上的快取策略都要重新想。
強一致性:電商賣十件庫存,兩個人同時下單各扣一件,皆大歡喜。拍賣同一件商品兩個人同時出到 10,000 元,只能有一個人成立,而且系統必須明確決定是誰、順序如何。這是典型的競態條件問題。
時間是業務邏輯:結標時間到了要自動結束、要決定得標者、要發通知、要開始計算付款期限。這些事沒有人會按按鈕觸發,全部得由系統自己準時做。而「準時」在分散式系統裡從來不是免費的。
搞清楚這三點之後再回頭看技術選型,判斷會清楚很多。
Laravel 適合做拍賣系統嗎?
適合,前提是你願意用它的事件系統與佇列,而不是把所有邏輯塞進 Controller。
Laravel 在拍賣場景的優勢是生態完整度:事件廣播、佇列、排程、快取鎖這些拍賣一定會用到的元件都內建,不需要東拼西湊。特別是 Laravel Reverb 出現之後,即時推播不再需要依賴外部服務,這對成本結構的影響不小。
要注意的是 PHP 的執行模型。PHP 是請求結束就釋放的短生命週期,這對「維持大量長連線」不友善——但這件事在有了 Reverb 這類獨立 WebSocket 伺服器之後就不再是問題,因為長連線由它處理,PHP 只負責業務邏輯。
跟其他選擇比較:
| 技術棧 | 即時性 | 開發速度 | 人力好找 | 適合情境 |
|---|---|---|---|---|
| Laravel + Reverb | 好 | 快 | 台灣容易找 | 中小型拍賣平台、快速上線 |
| Node.js 全棧 | 很好 | 中等 | 中等 | 極高併發、即時性要求嚴苛 |
| Go 服務 | 很好 | 慢 | 台灣較難找 | 大型平台、有專職架構團隊 |
| 現成 SaaS | 視方案 | 最快 | 不需要 | 驗證商業模式階段 |
如果還在評估要自建還是租用現成方案,自架網站與第三方平台的取捨那篇有更完整的成本比較。技術選型本身沒有標準答案,但選型錯誤的代價在拍賣專案裡特別高,因為核心邏輯全部綁在資料結構上。
拍賣系統的核心資料結構怎麼設計?
最關鍵的一個決定是:出價紀錄採用「只新增不修改」的設計,永遠不要去更新它。
很多人第一版會這樣做:一張 auctions 表存商品,裡面有個 current_price 欄位,每次有人出價就更新它。這樣寫最直覺,也最快出事——併發時兩個請求都讀到舊價格,都覺得自己出價有效。
正確的做法是把出價當成事件流:每一次出價都是一筆新紀錄,包含出價者、金額、時間、以及當下的狀態。這張表只寫入不更新、不刪除。目前最高價是「查出來的結果」,不是「存起來的欄位」。
這個設計有幾個好處遠超過它的成本:
- 爭議可追溯:買家說「我明明有出價」,你可以拿出完整時間軸
- 稽核合規:拍賣涉及金流,出價紀錄被竄改過的系統在爭議處理上站不住腳
- 重算能力:規則改了、發現 bug 了,可以拿原始紀錄重新計算結果
- 代理出價好實作:自動出價需要知道每個人的最高授權額度與出價順序
代價是查詢會慢一點,這用快取和適當的索引可以解決——而且這是可以事後優化的問題,不像資料結構錯了要整個重寫。關於索引與查詢效能的細節,拍賣網站的資料庫設計講得比較深入。
另外提醒一個小地方但影響很大:所有金額欄位用整數存,單位是「分」。用浮點數存錢,遲早會出現 0.1 + 0.2 不等於 0.3 的經典問題,而在拍賣裡這種誤差會直接變成客訴。
即時出價要怎麼推到每個人的瀏覽器?
核心決策只有一個:這個廣播事件要不要進佇列。答案是不要。
Laravel 的事件廣播預設會丟進佇列,由 worker 非同步處理。這在寄信、產報表這類場景是好事,因為使用者不需要立刻知道結果。但出價廣播完全相反——延遲一秒,使用者就會覺得系統壞了。
而佇列的延遲是不可控的。如果佇列裡塞了兩百封待寄的通知信,你的出價廣播就得排在後面。所以出價事件必須明確標示為「立即廣播」,跳過佇列直接推出去。
至於通知信、寫入統計、更新排行榜這些,就該進佇列——它們晚個三十秒沒人在意。判斷準則是:使用者正在盯著螢幕等這件事發生嗎? 是就同步,不是就進佇列。
前端這邊,Livewire 搭配事件監聽可以讓元件在收到廣播時自動重繪,不需要自己寫一堆 JavaScript 去接 WebSocket 訊息。這是這套組合最省事的地方——出價元件收到「有新出價」的事件,重新渲染目前價格和出價紀錄,整件事寫起來意外地短。
WebSocket 的連線管理、認證與擴展細節,可以參考即時競標的 WebSocket 架構。
併發出價:最容易被低估的部分
兩個人在同一毫秒出價,如果沒有鎖,你會得到兩個「最高出價者」,而且資料庫裡完全看不出哪裡錯了。
這是拍賣系統最經典的競態條件。流程大概是這樣出問題的:A 讀到目前最高價 10,000,B 也讀到 10,000,A 寫入 10,500,B 也寫入 10,500,兩筆都成功——系統現在有兩個相同金額的最高出價,而拍賣規則說出價必須高於前一筆。
解法是在「檢查目前最高價」到「寫入新出價」這整段之間上鎖,而且鎖的粒度要對:鎖到單一拍賣品,不是鎖整張表。鎖整張表的話,五百件同時進行的拍賣會互相排隊,效能直接崩掉。
Laravel 的快取原子鎖很適合做這件事,因為它跨程序有效(多台機器共用 Redis),而且可以設定持有時間上限,避免某個請求掛掉之後鎖永遠不釋放。
實務上還有幾個容易忽略的點:
- 鎖等待時間要短:等超過一兩秒就該告訴使用者「出價失敗請重試」,而不是讓他一直轉圈
- 失敗要給明確訊息:「已有人出到更高價」比「系統忙碌中」有用一百倍
- 重試不要自動做:使用者的出價意願是有上限的,系統不該替他自動加價
這一段是我認為整個拍賣系統最該寫測試的地方。 併發問題的特性是平常不會發生,一發生就是在流量最高的結標前一刻,而且事後很難重現。
結標怎麼觸發?三種做法的取捨
時間到了要有人關掉它,而「誰來關」這個問題有三種答案,各有明顯的優缺點。
方案一:定時排程掃描。 每分鐘跑一次,找出所有已到期未結標的拍賣,逐一處理。優點是實作簡單、掛掉了下一輪會補做。缺點是精度只有一分鐘,而且拍賣量大時每次掃描都是負擔。
方案二:延遲佇列任務。 拍賣建立時就排一個「在結標時間執行」的任務。精度可以到秒級,資源消耗也小。缺點是拍賣時間被延長時(防狙擊機制觸發),要記得取消舊任務並排新的,這裡很容易出 bug。
方案三:兩者並用。 用延遲任務負責準時結標,同時保留一個低頻率的排程做兜底,撿漏掉的。這是實務上最常見的做法,因為它同時解決了精度和可靠性。
| 方案 | 精度 | 可靠性 | 實作複雜度 | 適合規模 |
|---|---|---|---|---|
| 定時排程掃描 | 分鐘級 | 高 | 低 | 小型、對秒數不敏感 |
| 延遲佇列任務 | 秒級 | 中 | 中 | 中型 |
| 兩者並用 | 秒級 | 高 | 高 | 正式營運平台 |
這裡還有個常被忽略的設計:結標時間不該是固定不變的。防狙擊機制會在最後幾分鐘有人出價時自動延長,這代表你的結標觸發機制必須能被動態改期。這個需求如果沒在一開始考慮進去,後面補會很痛苦——防狙擊機制的完整運作原理可以參考。
開發時程與人力要抓多少?
一個能真正上線營運的拍賣平台,抓四到六個月、兩到三人,這是保守但誠實的估法。
拆開來看大概是這樣:
- 基礎建設與資料結構(3–4 週):使用者、商品、分類、後台骨架。這段看起來慢,但資料結構定不好後面全部要改
- 拍賣核心(6–8 週):出價、代理出價、加價規則、併發控制、結標流程。這是最難也最該花時間的部分
- 即時推播(2–3 週):WebSocket 伺服器、廣播事件、前端元件連動
- 交易與金流(4–6 週):付款、物流、手續費計算、發票。多商家平台的話這段會再長一些
- 後台與營運工具(3–4 週):商品審核、糾紛處理、數據報表
- 測試與壓力測試(2–3 週):特別是併發與結標的邊界情況
如果是多商家平台,還要加上商家入駐、抽成結算、對帳這些模組,時程會再往上加一到兩個月。
最常見的低估來源是「營運工具」。很多團隊把力氣全花在前台體驗,上線後才發現客服沒有工具處理糾紛、財務沒有報表對帳、審核商品要直接進資料庫改——然後花三個月補這些東西。
真的要評估專案規模,建議先把上面這六塊分別估一次,再乘以 1.3 當緩衝。或者找有實際做過拍賣平台開發的團隊聊一輪,通常一小時的對話就能省下不少試錯成本。
常見問題
Q:一定要用 WebSocket 嗎?輪詢不行嗎? 小規模可以先用輪詢撐著,但成本會隨人數線性上升——一百個人每兩秒問一次,就是每秒五十個請求打在同一個查詢上。而且輪詢的體感延遲明顯,在結標前那幾秒特別致命。過渡期可以用,長期不建議。
Q:出價紀錄只增不刪,資料量會不會爆掉? 會長很快,但這是可控的。熱門拍賣一件可能有數百筆出價,一年下來確實不小。做法是定期把已結標超過一定期間的拍賣資料歸檔到冷儲存,主表只留進行中與近期的。歸檔不等於刪除,法遵和爭議處理都需要它。
Q:可以用現成的 Laravel 拍賣套件嗎? 可以當作學習參考,但正式營運不建議直接用。拍賣的業務規則差異極大——加價級距、防狙擊策略、手續費計算、會員等級,每個平台都不一樣,而這些恰好是套件最難泛化的部分。改套件的成本經常高於自己寫。
Q:測試環境怎麼模擬併發出價? 寫測試時直接在同一個測試案例裡開多個並行請求打同一個拍賣品,斷言最終只有一筆出價成立、且金額順序正確。這類測試跑起來比較慢,但它是唯一能在上線前抓到競態條件的方法。
結語
用 Laravel 做拍賣系統這件事,技術上沒有什麼過不去的關卡,該有的工具都在。真正決定成敗的是你有沒有在一開始就認清這三件事:出價紀錄不能改、廣播不能排隊、時間必須準。
這三個決定都在專案最前面,也都是選錯了要重寫的類型。相對地,效能不夠可以加機器、介面不好看可以改、功能不足可以補——那些都是後面能處理的。
如果你正要開始一個拍賣專案,我的建議是:先花兩週把資料結構和併發策略想清楚再動手。這兩週看起來像沒有產出,但它會決定後面四個月順不順。