mybid
拍賣知識 · 9 分鐘閱讀 · 3 次閱讀

併發出價:兩個人在同一秒出價,系統到底該怎麼辦

結標前最後一秒同時湧入的出價,是拍賣系統最容易出錯也最難重現的地方。這篇拆解競態條件的成因與三種解法的取捨。

併發出價:兩個人在同一秒出價,系統到底該怎麼辦

一句話總結:當兩筆出價在極短時間內同時進來,系統如果沒有把「讀取目前最高價」到「寫入新出價」這段過程鎖起來,就會產生兩個相同金額的得標者,而這個錯誤在資料庫裡看起來完全正常,只有在客訴時才會被發現。

有一次幫朋友的平台看客訴,兩個買家同時堅稱自己是得標者。我調出出價紀錄,兩筆出價金額一模一樣,時間差 0.03 秒,狀態都是「有效」。

系統沒有壞掉——它非常忠實地執行了程式碼寫的邏輯。問題是那段邏輯假設了「出價是一個接一個發生的」,而現實裡它們是同時發生的。

這是拍賣系統最經典的一類錯誤,它平常不會出現,只在流量最高的結標前那幾秒發作,而且事後幾乎無法重現。 這篇就講清楚它為什麼會發生、有哪些解法、以及怎麼確認自己的系統沒這個問題。

併發出價問題是什麼?

併發出價問題(Race Condition in Bidding)是指兩個以上的出價請求在時間上重疊執行,導致它們讀到相同的舊狀態,各自做出「我的出價有效」的判斷。

用最白話的方式描述那 0.03 秒發生的事:

系統處理一筆出價,做的其實是三個動作——先查目前最高價多少、再判斷新出價有沒有比它高、最後把新出價寫進去。這三個動作寫在同一個函式裡,看起來像是一氣呵成的。

但在伺服器上,這三個動作之間是有縫隙的。A 的請求做完第一步、還沒走到第三步的時候,B 的請求進來也做了第一步。於是兩個人都看到「目前最高價 10,000」,兩個人都認為出 10,500 是合法的,兩筆都寫進去了。

結果:資料庫裡有兩筆 10,500 元的有效出價。每一筆單獨看都完全正確,合在一起就是錯的。

併發出價的競態條件時序圖:兩筆請求在讀取與寫入之間交錯,導致兩人都判斷自己出價有效

這件事的發生機率跟同時出價的人數平方成正比。平常兩三個人在看,幾乎不會遇到;結標前三十秒五十個人在搶,就變成必然事件。

為什麼資料庫的交易機制不夠?

因為交易保證的是「這幾個寫入要嘛全成功要嘛全失敗」,不保證「我讀到的資料在我寫入前沒被別人改過」。

這是很多人第一次遇到這個問題時的直覺解法:把三個動作包進一個交易裡就好了吧?

不夠。交易解決的是原子性——如果寫到一半失敗,前面的會被回滾,不會留下半套資料。但兩個交易可以同時進行,各自讀到相同的舊值,然後各自成功提交。交易沒有被違反,錯誤還是發生了。

要靠資料庫解決的話,得用更強的隔離層級或明確的資料列鎖定,但這在拍賣場景有兩個實際困擾:

第一,鎖住的時間可能比你想的長。 出價流程裡除了寫入之外,還可能要計算代理出價、判斷是否觸發防狙擊延長、更新結標時間。這些邏輯在交易裡執行,資料列就一直被鎖著,其他請求全部排隊。

第二,跨資源的操作沒辦法納入交易。 出價成功後要把新價格廣播出去、要寫快取。這些不是資料庫操作,交易回滾時它們不會跟著回滾。

所以實務上更常見的做法是在應用層加一把明確的鎖,把整段業務邏輯保護起來,而不是只依賴資料庫的交易語意。

鎖該鎖多大?粒度決定效能

鎖到「單一拍賣品」這個層級,不要鎖整張出價表——這是效能能不能撐住的分水嶺。

鎖的粒度有個直覺但錯誤的選法:既然要避免衝突,鎖大一點比較安全吧?

不行。假設平台上同時有五百件拍賣在進行,如果每次出價都要取得同一把「出價鎖」,那五百件商品的所有出價會排成一條隊伍。A 商品的出價要等 B 商品的出價處理完——它們明明毫無關係。結標高峰時這條隊伍會長到使用者全部逾時。

正確的粒度是每件拍賣品一把獨立的鎖。同一件商品的出價互相排隊(這是必要的),不同商品之間完全不干擾。五百件商品就是五百把互不相干的鎖,可以完全平行處理。

鎖的粒度 正確性 併發效能 適用情況
鎖整張表 正確 極差 不建議
鎖單一拍賣品 正確 標準做法
不鎖 錯誤 最好 只有讀取才可以

實作上,Laravel 的快取原子鎖很適合這個場景,因為它是跨程序有效的——多台網頁伺服器共用同一個 Redis,鎖就能跨機器生效。如果只用 PHP 程序內的鎖,多台機器的環境下等於沒鎖。

鎖粒度的對比:鎖整張表會讓所有商品的出價排成一列,鎖單一拍賣品則能完全平行處理

還有兩個容易忽略的設定:

鎖要有存活時間上限。 如果某個請求在持有鎖的時候當掉了,鎖沒被釋放,那件商品就再也沒人能出價。設一個上限(例如五秒),時間到自動釋放。

等鎖的時間要短。 取不到鎖時不要無限等待。等超過一兩秒就直接告訴使用者失敗請重試——在拍賣情境下,讓使用者轉圈十秒比直接告訴他失敗更糟糕,因為那十秒裡價格早就變了。

除了上鎖,還有哪些做法?

主流有三種解法,各自適合不同的規模與團隊。

做法一:悲觀鎖。 就是前面講的——先取得鎖再處理。優點是邏輯直觀、正確性容易驗證。缺點是有鎖的開銷,而且極端高併發下鎖本身會變成瓶頸。中小型平台建議直接選這個,簡單可靠。

做法二:樂觀鎖。 不預先上鎖,寫入時附帶「我讀到的版本號」,如果資料在這期間被改過就寫入失敗、請客戶端重試。優點是沒有鎖開銷,讀多寫少時效能好。缺點是衝突時要重試,而拍賣結標前恰好是「寫入極度密集」的情境,重試風暴反而更糟。

做法三:佇列序列化。 把同一件商品的所有出價丟進一個專屬佇列,由單一消費者依序處理。這樣天然就不會有併發問題,因為根本沒有同時執行。優點是正確性最強、順序明確。缺點是架構複雜,而且出價變成非同步的——使用者按下按鈕後不會立刻知道結果,需要另外設計狀態回報。

做法 正確性 高併發表現 實作複雜度 使用者立即得知結果
悲觀鎖
樂觀鎖 寫入密集時差
佇列序列化 最高 否,需額外設計

併發出價三種解法的比較:悲觀鎖、樂觀鎖與佇列序列化在正確性、效能與複雜度上的取捨

我的建議是:先用悲觀鎖把正確性做對,等真的撞到效能天花板再考慮換。 併發問題的代價是資料錯誤和客訴,而效能問題的代價只是慢——先解決前者永遠是對的順序。這個決策邏輯跟拍賣系統整體的架構取捨是一致的。

出價失敗時該給使用者看什麼?

「已有人出到更高價,目前是 10,800 元」比「系統忙碌中,請稍後再試」有用一百倍。

這是技術問題裡少數直接影響體驗的部分,卻經常被當成邊角料處理。

當出價因為併發而失敗時,使用者的心理狀態是緊繃的——他正在搶東西,倒數在跑。這時候給他一個模糊的錯誤訊息,他會做兩件事:瘋狂重試(增加系統負擔),或是直接放棄(你損失一次成交)。

好的失敗訊息要包含三個資訊:發生了什麼、現在的狀態是什麼、他可以做什麼。例如「出價失敗:已有人出到 10,800 元。要出 11,000 元嗎?」並附上一個按鈕。這樣使用者不用重新輸入、不用重新整理頁面,情緒也不會失控。

另外一個重要原則:不要替使用者自動重試加價。系統自動幫他往上加,看起來很貼心,實際上會讓他花超出預算的錢——這在消費爭議上站不住腳,也會嚴重傷害信任。出價金額必須是使用者的明確決定。

怎麼測試一個平常不會發生的問題?

手動測試永遠測不到併發問題,必須寫自動化測試主動製造衝突。

具體做法是在測試裡對同一件拍賣品同時發出多個出價請求,然後斷言三件事:

  1. 最終只有一筆出價成立
  2. 成立的那筆是金額最高的(或依規則該勝出的那筆)
  3. 其他筆都得到明確的失敗狀態,而不是靜默消失

這類測試跑起來比一般測試慢,也比較難寫,但它是唯一能在上線前抓到競態條件的方法。我會建議把它放在必跑的測試集裡,而不是那種「有空才跑」的分類——因為併發相關的程式碼特別容易在後續重構時被改壞,而改壞了不會有任何徵兆。

除了單元測試,上線前也值得做一次壓力測試:模擬結標前三十秒的流量尖峰,看看鎖的等待時間、失敗率、以及資料是否仍然正確。特別要測防狙擊延長觸發時的併發情況——那是兩個複雜機制同時作用的時刻,也是最容易出事的地方。

常見問題

Q:小平台流量不大,可以先不處理併發嗎? 不建議。併發問題的觸發條件不是「平均流量高」而是「瞬間有人同時出價」,這在任何規模的平台上都會發生——只要有兩個人搶同一件商品。而且這是少數「一開始沒做,之後補會很痛」的東西,因為出價邏輯會被其他功能依賴。

Q:出價紀錄採用只新增不修改的設計,是不是就不用鎖了? 不是。只新增不修改的資料結構解決的是「歷史被覆蓋」的問題,讓紀錄可追溯;併發解決的是「同時寫入導致邏輯錯誤」。兩者互補,都要做。實際上正因為紀錄完整,併發出錯時才有辦法事後查證是哪裡出的問題。

Q:使用者連續快速點擊出價按鈕,也算併發嗎? 算,而且很常見。除了後端要有鎖,前端也該在送出後立刻禁用按鈕直到收到回應。這是最便宜的一道防線,能過濾掉大部分的重複請求。

Q:代理出價會讓併發問題更複雜嗎? 會。因為一筆手動出價可能連鎖觸發多筆系統自動出價,這整串連鎖反應都必須在同一把鎖的保護下完成。如果中途放開鎖讓別人插隊,最終的出價順序就會亂掉。設計代理出價時,務必把整段連鎖計算視為一個不可分割的操作。

結語

併發出價這件事的難處不在技術本身——加一把鎖並不困難。難的是它不會提醒你它壞了。系統不會報錯、監控不會告警、日誌看起來一切正常,直到某天兩個買家同時來客訴。

所以我對這一塊的態度比其他部分保守很多:寧可效能差一點,也要把正確性焊死。拍賣平台賣的說到底是信任,而「同一件東西賣給兩個人」這種事,發生一次就夠讓人記很久。

如果你正在做拍賣系統,這是我會建議最優先寫測試的地方。不是因為它最容易錯,而是因為它錯了最難發現。

相關文章

AI 拍賣估價實戰:系統怎麼運作、到底準不準、能不能自己做

AI 估價已經進到拍賣平台的日常功能,但它在哪些商品上準、哪些商品上會慘敗?這篇拆解運作原理、實測誤差與自建門檻。

· 10 分鐘 · 1 次閱讀
閱讀 →

拍賣要繳稅嗎?台灣拍賣交易的稅務常識

在拍賣平台上買賣東西到底要不要繳稅?個人偶爾賣和經常性銷售差很多,搞不清楚可能被國稅局找上門。這篇用白話講清楚台灣拍賣交易的稅務眉角。

· 6 分鐘 · 187 次閱讀
閱讀 →

拍賣棄標全解析:搞懂處罰規則,避免帳號被停權

完整解析拍賣棄標的定義、處罰方式、棄標率計算與申訴流程,幫你避開停權地雷,保護帳號信譽。

· 7 分鐘 · 1 次閱讀
閱讀 →