免費完整指南

票務系統

設計票務系統。請涵蓋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...

00

練習檢查點

面試節奏保持精簡,讓頁面能把注意力花在真正的設計決策上。

  1. 01
    釐清範圍
  2. 02
    需求 + 規模
  3. 03
    API + 資料模型
  4. 04
    畫出架構
  5. 05
    深入探討
  6. 06
    取捨決策
01

形塑設計的需求

不要只是陳述需求,要主動問出來。每張卡把設計限制和一句你可以在畫架構前說出口的釐清問句配成一組。

功能需求

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 絕不能重複收款或重複訂票——確認兩次的效果必須跟確認一次相同。

持續追問 —— 面試是一場對話

真實面試探得比一份整齊清單深得多。這些範圍問句,區分出真正拷問問題的人和只是背誦的人。

  • 用座位圖的對號入座,還是用容量計數器的自由入場——還是兩者都要?
  • 退票或取消會把座位放回銷售嗎,還是不在範圍內?
  • 每場活動有沒有每位使用者的購票上限需要我們強制執行?
  • 卡片資料是我們自己處理,還是交給金流商——PCI 合規會落在我們頭上嗎?
  • 機器人與黃牛的防制在範圍內嗎?
02

逼出架構決策的數字

把每個估算都當成一種壓力,用來合理化一個元件:快取、佇列、分片、副本、worker pool 或退路。

01

開賣爭用

約 1,000 萬名使用者(來自開賣需求)在同一開賣瞬間爭搶假設的約 5 萬個座位10,000,000 ÷ 50,000 ≈ 每個座位 200 名使用者

幾乎所有人都得先進虛擬等候室排隊——由放行機制決定誰才能走到訂票這一步。

02

預留寫入突發

開賣的第一分鐘:獲准進入的使用者各自嘗試一次保留放行 50K 使用者/分鐘 ≈ 對單一活動分區每秒 800 次預留嘗試

每活動分區加上短的原子狀態轉換,讓熱分區撐得住。其他活動不受影響。

03

瀏覽讀取偏斜

讀取密集約 100:1——座位圖輪詢佔主導800 writes/s × 100 ≈ 尖峰每秒 80K 次座位圖讀取

以快取/CDN 提供檢視,容許幾秒的過時——庫存資料庫永遠看不到瀏覽流量。

04

保留 TTL vs 付款延遲

外部付款確認的 p95 是幾秒;使用者填表要幾分鐘TTL 5-10 min ≫ payment p95 ~3-10 s

寬鬆的 TTL 避免保留在付款途中過期;過期會自動釋放被放棄的座位,不需清理工作。

05

搜尋延遲預算

搜尋目標端到端 < 500 ms反向索引查詢 ~50-100 ms + 排名 + 網路 ≈ 遠低於 500 ms

需要全文索引(而非 LIKE 掃描);快取重複查詢,並用 CDN 快取非個人化的結果。

決策範例

數字

想像開賣:約 10M 人想搶約 50K 個座位——每個座位約 200 人。這場仗打的是每個座位的狀態,所以對同一個座位的寫入必須一次一筆地進行。同時,所有只是在瀏覽的人都由快取服務。

我的選擇

我會做三件事。第一,把所有人放進虛擬等候室,以受控的速率放行進入訂票流程,讓座位資料庫永遠只看到它撐得住的流量。第二,當使用者選好座位,就保留 5-10 分鐘——在這段時間內付款,否則座位自動重新開賣。第三,把「檢查座位是空的 AND 把它標記為 held」做成單一的原子步驟(一個 row lock,或一個只有在中間沒人改動座位時才成功的更新),這樣兩個買家永遠不可能都搶到它。

避免

我不會做的事:先讀「座位是空的」,再以第二個步驟把它標記為 held。在這兩個步驟之間的空隙裡,另一個買家可以做同樣的讀取——兩人都以為自己贏了,座位就售出兩次。檢查與更新必須是一個步驟,而落敗的一方應該得到清楚的「有人比你先一步」回應(HTTP 409),而不是無聲的失敗。

何時改變

如果場館是自由入座、沒有特定座位、只有一個容量數字,每座位鎖定根本是多此一舉。我會保一個剩餘容量的原子計數器,歸零時就停止販售。

03

架構路徑

先看一張完整的圖,再把每條路徑各自畫成一張圖 —— 寫入路徑與讀取路徑承載不同流量,合理化不同的元件。

完整全貌

總覽 —— 每個元件

ClientAdmissionControlBooking ServiceHold Store (TTL)Seat InventoryDBPayment Service冪等確認Seat-Map Cache +Search瀏覽/搜尋讀取

訂票是主幹;瀏覽與搜尋永遠不碰座位庫存(虛線 = 讀取端,在訂票熱路徑之外)。過期的保留會自動歸還座位。

路徑 1

訂票路徑——先預留,再確認

ClientAdmissionControlBooking ServiceHold Store (TTL)Seat InventoryDBPayment Service
  • 預留會放上一個帶倒數(TTL)的保留——買家一直沒付款,保留就過期,座位自己回到銷售中。
  • 確認收款並在單一原子步驟中把保留中的座位翻成已售出;重試確認絕不會重複收款或重複訂票。
  • 「這個座位是空的嗎?」和「把它標成保留」必須是同一個步驟——一個 row lock,或一個只有在座位沒被人動過時才成功的更新(compare-and-set)。
  • 如果那是兩個分開的步驟,兩個買家可以在中間的空隙裡雙雙通過檢查——座位就售出兩次。

路徑 2

瀏覽路徑——搜尋與座位圖(讀取密集)

ClientCDN / EdgeEvent + SearchServiceSeat-Map CacheDatabase

活動頁面預先渲染並由 CDN 快取,所以搶購潮永遠不會為了靜態內容打到 app 伺服器。可用性可以落後個幾秒。訂位交易才是真相被強制執行的地方。

04

API 與資料模型

在最佳化之前,先讓契約可被檢視:端點、實體、擁有權、重試與狀態。

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 都是安全的。

核心實體

Event

event_id (PK) · venue_id · performer · starts_at · on_sale_at

Ticket

ticket_id (PK) · event_id · section/row/seat · price · status: available/held/booked · version

status + version 驅動樂觀併發控制;依 event_id 分片,讓一場開賣不會拖垮其他活動。

Booking

booking_id (PK) · user_id · ticket_ids · total · status: pending/confirmed/failed · idempotency_key

User

user_id (PK) · email · created_at

05

深入方向

在面試最後三分之一挑一條路線。每條路線給你主題、它該回答的面試官問題,以及要避免的失敗模式。

重點

座位狀態

帶一個座位走過 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 為你的說明評分。

用 AI 練習這題 →