01使用者主要要做什麼——瀏覽並搜尋活動,還是只有訂票?
使用者可以查看活動及其場館座位圖與接近即時的可用狀態,並依關鍵字、日期、地點搜尋活動。
免費完整指南
設計票務系統。請涵蓋Seat inventory model and seat-map representation for a venue/event, including seat status transitions, Reservation hold with TTL: reserve then confirm two-phase flow, and auto-release when...
面試節奏保持精簡,讓頁面能把注意力花在真正的設計決策上。
不要只是陳述需求,要主動問出來。每張卡把設計限制和一句你可以在畫架構前說出口的釐清問句配成一組。
01使用者主要要做什麼——瀏覽並搜尋活動,還是只有訂票?
使用者可以查看活動及其場館座位圖與接近即時的可用狀態,並依關鍵字、日期、地點搜尋活動。
02當使用者選好座位,付款期間會拿到暫時保留嗎?
使用者可以選座位,並拿到一個短命的保留(5-10 分鐘)。他們先保留,再用付款確認。一個從未確認的保留會自動釋放。
03有哪一條保證是我們絕對不能打破的?
訂票一旦成立即為最終:每個座位只會售出一次——永遠不會重複售出,即使在秒殺開賣期間也一樣。
04座位圖選位、best-available 配位,還是兩者都要?
兩者都要。對號入座場館用選位圖,加上最佳可選座。就算是最佳可選座,也是對特定座位做原子保留,絕不是一個模糊的數量。
05買家可以在結帳途中把座位加進既有的保留嗎?
可以,只要保留還沒過期。加進來的座位各自拿自己的保留,並原子地併進同一筆訂位。它們要嘛一起確認,要嘛這次追加就乾淨地失敗。
06賣光了——我們需要候補清單嗎?
第一天不在範圍內——但候補清單(waitlist)是很可能的後續,所以第一天的選擇不該讓之後加上它變得痛苦。
範圍外熱門活動的動態定價 · 建立活動用的後台與活動統籌工具 · 查看歷史訂票、票券轉讓與轉售
01哪裡需要強一致性,哪裡的資料可以是過時的?
訂位需要強一致性,這樣一個座位絕不會賣兩次。瀏覽與搜尋只需要可用性。選位圖可以比現實落後個幾秒。
02秒殺開賣的尖峰長什麼樣子?
一場熱門活動在開賣的那一刻可能吸引約 10M 名使用者。系統必須吸收那個尖峰。讓使用者公平地排隊等,比讓他們失敗好。
03瀏覽與搜尋要感覺多快?
搜尋在約 500 ms 內;座位圖檢視即使在搶購潮中也必須感覺瞬間完成。整體讀取密集,約 100:1。
04保留能維持多久,過期時會發生什麼?
保留很短(5-10 分鐘),並以 TTL 自動釋放。被放棄的結帳會歸還座位,不需要手動清理。保留也必須活得比付款供應商的延遲久。
05如果一個確認送達兩次——連點兩下,或金流商重試 webhook——收款與售出仍然必須只發生一次嗎?
重試與重複的付款 webhook 絕不能重複收款或重複訂票——確認兩次的效果必須跟確認一次相同。
真實面試探得比一份整齊清單深得多。這些範圍問句,區分出真正拷問問題的人和只是背誦的人。
把每個估算都當成一種壓力,用來合理化一個元件:快取、佇列、分片、副本、worker pool 或退路。
開賣爭用
約 1,000 萬名使用者(來自開賣需求)在同一開賣瞬間爭搶假設的約 5 萬個座位10,000,000 ÷ 50,000 ≈ 每個座位 200 名使用者
幾乎所有人都得先進虛擬等候室排隊——由放行機制決定誰才能走到訂票這一步。
預留寫入突發
開賣的第一分鐘:獲准進入的使用者各自嘗試一次保留放行 50K 使用者/分鐘 ≈ 對單一活動分區每秒 800 次預留嘗試
每活動分區加上短的原子狀態轉換,讓熱分區撐得住。其他活動不受影響。
瀏覽讀取偏斜
讀取密集約 100:1——座位圖輪詢佔主導800 writes/s × 100 ≈ 尖峰每秒 80K 次座位圖讀取
以快取/CDN 提供檢視,容許幾秒的過時——庫存資料庫永遠看不到瀏覽流量。
保留 TTL vs 付款延遲
外部付款確認的 p95 是幾秒;使用者填表要幾分鐘TTL 5-10 min ≫ payment p95 ~3-10 s
寬鬆的 TTL 避免保留在付款途中過期;過期會自動釋放被放棄的座位,不需清理工作。
搜尋延遲預算
搜尋目標端到端 < 500 ms反向索引查詢 ~50-100 ms + 排名 + 網路 ≈ 遠低於 500 ms
需要全文索引(而非 LIKE 掃描);快取重複查詢,並用 CDN 快取非個人化的結果。
決策範例
想像開賣:約 10M 人想搶約 50K 個座位——每個座位約 200 人。這場仗打的是每個座位的狀態,所以對同一個座位的寫入必須一次一筆地進行。同時,所有只是在瀏覽的人都由快取服務。
我會做三件事。第一,把所有人放進虛擬等候室,以受控的速率放行進入訂票流程,讓座位資料庫永遠只看到它撐得住的流量。第二,當使用者選好座位,就保留 5-10 分鐘——在這段時間內付款,否則座位自動重新開賣。第三,把「檢查座位是空的 AND 把它標記為 held」做成單一的原子步驟(一個 row lock,或一個只有在中間沒人改動座位時才成功的更新),這樣兩個買家永遠不可能都搶到它。
我不會做的事:先讀「座位是空的」,再以第二個步驟把它標記為 held。在這兩個步驟之間的空隙裡,另一個買家可以做同樣的讀取——兩人都以為自己贏了,座位就售出兩次。檢查與更新必須是一個步驟,而落敗的一方應該得到清楚的「有人比你先一步」回應(HTTP 409),而不是無聲的失敗。
如果場館是自由入座、沒有特定座位、只有一個容量數字,每座位鎖定根本是多此一舉。我會保一個剩餘容量的原子計數器,歸零時就停止販售。
先看一張完整的圖,再把每條路徑各自畫成一張圖 —— 寫入路徑與讀取路徑承載不同流量,合理化不同的元件。
完整全貌
訂票是主幹;瀏覽與搜尋永遠不碰座位庫存(虛線 = 讀取端,在訂票熱路徑之外)。過期的保留會自動歸還座位。
路徑 1
路徑 2
活動頁面預先渲染並由 CDN 快取,所以搶購潮永遠不會為了靜態內容打到 app 伺服器。可用性可以落後個幾秒。訂位交易才是真相被強制執行的地方。
在最佳化之前,先讓契約可被檢視:端點、實體、擁有權、重試與狀態。
GET/events/{event_id}
回應200 { event, venue, seat_map, availability }
快取/CDN 優先;可用狀態可能落後幾秒——訂票才是真相被強制執行的地方。
GET/events/search?keyword&date&location
回應200 Event[]
反向索引(全文檢索)——含模糊比對也在 500 ms 以內。
POST/bookings
請求{ event_id, ticket_ids[] }
回應201 { booking_id, hold_expires_at } · 409 座位已被保留或售出
建立 TTL 保留:座位原子性地翻成 held,否則整個請求失敗——不會有部分保留。
POST/bookings/{booking_id}/confirm
請求{ payment_token, idempotency_key }
回應200 已確認 · 402 付款失敗(保留繼續倒數) · 410 保留已過期
以 booking_id 為單位冪等:金流商的重試與重複的 webhook 都是安全的。
核心實體
Eventevent_id (PK) · venue_id · performer · starts_at · on_sale_at
Ticketticket_id (PK) · event_id · section/row/seat · price · status: available/held/booked · version
status + version 驅動樂觀併發控制;依 event_id 分片,讓一場開賣不會拖垮其他活動。
Bookingbooking_id (PK) · user_id · ticket_ids · total · status: pending/confirmed/failed · idempotency_key
Useruser_id (PK) · email · created_at
在面試最後三分之一挑一條路線。每條路線給你主題、它該回答的面試官問題,以及要避免的失敗模式。
帶一個座位走過 available → held → sold。到底是什麼在翻動每個狀態,兩個人有沒有可能保留同一個座位?
一次原子寫入把座位從 available 翻成 held。只有座位還 available 時才會成功,所以第二個買家就直接失敗。兩個人不可能同時占住同一個座位,因為檢查和翻轉是同一步。付款後,confirm 把 held 變成 sold;TTL 過期則把它退回 available。
沒有 TTL 的保留——被放棄的購物車會永遠鎖住座位。
為什麼要在付款前保留座位,如果付款一直沒完成,座位會怎樣?
先占住座位,讓買家在輸入卡片資料時它一直是他的。如果你在預留當下就標成 sold,每一筆失敗或放棄的付款都會卡住一個賣不掉的座位。付款一直沒完成時,TTL 會讓這個 hold 過期,座位自動回到販售中——不需要清理工作排程。
在預留時就把座位標記為售出——付款失敗後它就變得無法販售。
Row lock、版本檢查(CAS)還是原子計數器——各自如何阻止兩個買家贏得同一個座位,在爭用下各自的代價是什麼?
對號入座就用短暫的 row lock,或單一語句的條件式更新,再用准入控制(admission control)把爭用壓下來。row lock 會把買家序列化:永遠正確,但他們會在熱門的列上排隊。版本檢查(CAS)不用鎖,但在每個座位約 200 個買家時,大多數會重試而且一直輸。原子計數器只適合可互換的座位——自由入座。
秒殺中純用樂觀重試——大多數買家一次又一次失敗,而不是公平地排隊等候。
1,000 萬人湧向一場開賣。你如何讓他們公平地等候,而不是把大多數人以錯誤擋掉?
把全部 1,000 萬人放進一個候位室,再用受控的速率放行。一個 token bucket 大約每分鐘放 5 萬個使用者進訂票流程,所以座位庫存看到的流量永遠都在它撐得住的範圍內。其他人保有一個公平的排隊位置,而不是對著錯誤狂按重試。其他活動完全感覺不到這波尖峰。
讓整群人都抵達座位庫存——等候室的存在就是為了保護它。
確認請求送達兩次——一次重試或一次連點兩下。你如何確保卡片只被扣款一次、座位只售出一次?
confirm 帶著一個 idempotency key、booking_id,而服務會把第一次嘗試的結果存起來。任何帶著同一個 key 的重試或重複 webhook,拿回來的是那個存好的結果,而不是再跑一次。所以不管這個請求到達幾次,卡只會被扣一次,座位也只會賣出一次。
確認上沒有 idempotency key——一次網路重試就會重複收款或重複售出。
把 票務系統 大聲講一遍,讓 AI 為你的說明評分。