概述#

設計一個線上電影票訂購系統,類似 BookMyShow,讓使用者可以在線上購買電影院座位。

  • 類似服務:bookmyshow.com、ticketmaster.com
  • 難度:困難

什麼是線上電影票訂購系統#

電影票訂購系統提供使用者在線上購買電影院座位的功能。電子票務系統允許使用者瀏覽目前上映的電影,並隨時隨地訂票。

系統需求與目標#

功能性需求#

  1. 系統能列出所有有合作電影院的城市
  2. 使用者選擇城市後,顯示該城市上映中的電影
  3. 使用者選擇電影後,顯示放映該電影的電影院與場次
  4. 使用者可選擇特定電影院的場次並訂票
  5. 系統顯示座位配置圖,使用者可依偏好選擇多個座位
  6. 使用者能區分已售出與可用座位
  7. 使用者可在付款前保留座位 5 分鐘
  8. 若座位可能釋出(例如其他使用者的保留到期),使用者可選擇等待
  9. 等待中的使用者依先到先服務(FCFS)原則處理

非功能性需求#

  1. 系統需具備高度並發處理能力,同一時間可能有多筆對同一座位的訂購請求,需優雅且公平地處理
  2. 核心功能涉及金融交易,系統必須安全,資料庫須符合 ACID 規範

設計考量#

  1. 為簡化設計,假設系統不需要使用者驗證
  2. 系統不處理部分訂票——使用者要麼取得所有想要的票,要麼一張都拿不到
  3. 公平性是系統的強制要求
  4. 為防止濫用,限制使用者單次不得訂超過 10 張票
  5. 熱門電影上映時流量會暴增,系統需具備可擴展性與高可用性

容量估算#

流量估算#

  • 每月 30 億 次頁面瀏覽
  • 每月售出 1,000 萬 張票

儲存估算#

假設 500 座城市,每座城市平均 10 間電影院,每間電影院 2,000 個座位,每天平均 2 場次。

每筆座位訂購需 50 bytes(含 ID、座位數、ShowID、MovieID、座位編號、狀態、時間戳等),電影與電影院資訊另需 50 bytes

500 城市 × 10 電影院 × 2,000 座位 × 2 場次 × 100 bytes = 1 GB/天

儲存五年的資料約需 3.6 PB

系統 API#

可使用 SOAP 或 REST API。以下為主要的 API 定義:

SearchMovies API#

SearchMovies(api_dev_key, keyword, city, lat_long, radius,
             start_datetime, end_datetime, postal_code,
             includeSpellcheck, results_per_page, sorting_order)

參數說明

  • api_dev_key(string):已註冊帳號的 API 開發者金鑰,用於流量控制等
  • keyword(string):搜尋關鍵字
  • city(string):依城市篩選
  • lat_long(string):依經緯度篩選
  • radius(number):搜尋半徑
  • start_datetime(string):篩選開始時間之後的電影
  • end_datetime(string):篩選結束時間之前的電影
  • postal_code(string):依郵遞區號篩選
  • includeSpellcheck(Enum: “yes” / “no”):是否在回應中包含拼字建議
  • results_per_page(number):每頁回傳筆數,最大 30
  • sorting_order(string):排序方式,例如 name,ascdate,descdistance,asc

回傳:JSON 格式的電影清單與場次資訊。

ReserveSeats API#

ReserveSeats(api_dev_key, session_id, movie_id, show_id, seats_to_reserve)

參數說明

  • api_dev_key(string):同上
  • session_id(string):使用者的 Session ID,用於追蹤預約,過期時自動移除
  • movie_id(string):電影 ID
  • show_id(string):場次 ID
  • seats_to_reserve(array):欲預約的座位 ID 陣列

回傳:預約狀態,可能為以下之一:

  1. Reservation Successful
  2. Reservation Failed - Show Full
  3. Reservation Failed - Retry, as other users are holding reserved seats

資料庫設計#

資料之間的關聯觀察:

  1. 每座城市可有多間電影院
  2. 每間電影院可有多個影廳
  3. 每部電影可有多個場次,每個場次可有多筆訂票
  4. 每位使用者可有多筆訂票

高層級設計#

  • Web Server:管理使用者 Session
  • Application Server:處理所有票務管理邏輯
  • Database:儲存資料
  • Cache Server:加速預約處理

詳細元件設計#

訂票工作流程#

  1. 使用者搜尋電影
  2. 使用者選擇電影
  3. 系統顯示該電影的可用場次
  4. 使用者選擇場次
  5. 使用者選擇欲預約的座位數
  6. 若所需座位可用,顯示影廳座位圖供使用者選座;若不可用,跳至步驟 8
  7. 使用者選擇座位後,系統嘗試預約這些座位
  8. 若座位無法預約,有以下情境:
    • 場次已滿:顯示錯誤訊息
    • 所選座位已被佔但仍有其他座位:返回座位圖重新選擇
    • 無可用座位,但有部分座位仍在其他使用者的預約池中(尚未完成付款):使用者進入等待頁面,可能出現以下結果:
      • 所需座位數釋出 → 導回座位圖選座
      • 所有座位已售出或預約池中座位不足 → 顯示錯誤訊息
      • 使用者取消等待 → 返回搜尋頁面
      • 最長等待 1 小時,超時後 Session 過期並返回搜尋頁面
  9. 預約成功後,使用者有 5 分鐘完成付款;付款完成則訂票確認。若超時未付款,所有預約座位釋出給其他使用者

系統需要兩個背景服務來追蹤預約狀態與等待佇列:ActiveReservationServiceWaitingUserService

圖 25.1:訂票流程圖(User ↔ Web Server ↔ Application Server ↔ Databases,含座位預留決策:ActiveReservationService + WaitingUserService)

ActiveReservationService#

將每個場次的所有預約保存在記憶體中的 Linked HashMap,同時也寫入資料庫:

  • 使用 Linked HashMap 的原因:可以快速跳到任一預約並移除(訂票完成時)
  • 每筆預約附帶過期時間,鏈結串列的頭部始終指向最舊的預約,方便檢查逾時
  • 外層使用 HashTable,Key 為 ShowID,Value 為包含 BookingIDTimestamp 的 Linked HashMap

圖 25.2:ActiveReservationsService 資料結構(Key=ShowID, Value=LinkedHashMap<BookingID, TimeStamp>)

資料庫中的狀態管理

  • 預約建立時,Booking 表的 Status 欄位設為 Reserved (1)
  • 訂票完成後,更新為 Booked (2),並從 Linked HashMap 中移除
  • 預約過期時,標記為 Expired (3) 或直接刪除,同時從記憶體中移除

ActiveReservationService 也與外部金融服務協作處理付款。每當訂票完成或預約過期,都會發送信號給 WaitingUserService,以便服務等待中的使用者。

WaitingUserService#

與 ActiveReservationService 類似,將每個場次的等待使用者保存在記憶體中的 Linked HashMap

  • 使用 Linked HashMap 可快速跳到並移除取消等待的使用者
  • 先到先服務原則,鏈結串列頭部始終指向等待最久的使用者
  • 外層 HashTable 的 Key 為 ShowID,Value 為包含 UserID 與等待開始時間的 Linked HashMap

客戶端可使用 Long Polling 持續更新預約狀態。當座位釋出時,伺服器透過此連線通知使用者。

預約過期處理#

  • 伺服器端由 ActiveReservationService 根據預約時間追蹤過期
  • 客戶端會顯示倒數計時器,可能與伺服器時間略有偏差
  • 伺服器端額外加上 5 秒緩衝,確保客戶端的計時器不會在伺服器之後才到期,避免使用者在伺服器已過期後仍嘗試付款的不良體驗

並發處理#

如何防止兩位使用者訂到同一個座位?

  • 使用 SQL 資料庫的 Transaction 機制避免衝突
  • 採用 Serializable 隔離級別(最高級別),可防止 Dirty Read、Nonrepeatable Read 與 Phantom Read
  • 在交易中讀取資料列時會取得寫入鎖,確保其他人無法同時修改

資料庫交易成功後,才開始在 ActiveReservationService 中追蹤該筆預約。

容錯機制#

  • ActiveReservationService 故障:可從 Booking 資料表中讀取所有 Status = Reserved (1) 的記錄來恢復;或採用 Master-Slave 架構,主節點故障時從節點接管
  • WaitingUserService 故障:由於等待使用者資料未存入資料庫,除非有 Master-Slave 架構,否則無法恢復
  • 資料庫也採用 Master-Slave 架構以確保容錯

資料分區#

資料庫分區#

  • 若依 MovieID 分區,同一部電影的所有場次會集中在同一台伺服器,熱門電影會造成負載過重
  • 較佳方案是依 ShowID 分區,讓負載分散到不同伺服器

ActiveReservationService 與 WaitingUserService 分區#

  • Web Server 管理使用者 Session 並處理通訊
  • 使用 Consistent HashingShowID 分配 Application Server
  • 同一場次的所有預約與等待使用者由特定伺服器群組處理
  • 假設 Consistent Hashing 為每個場次分配 3 台伺服器

預約過期時的處理流程

  1. 更新資料庫:移除或標記 Booking 為過期,更新 Show_Seats 表的座位狀態
  2. 從 Linked HashMap 中移除該筆預約
  3. 通知使用者預約已過期
  4. 向所有持有該場次等待使用者的 WaitingUserService 伺服器廣播,找出等待最久的使用者(透過 Consistent Hashing 得知伺服器位置)
  5. 通知持有等待最久使用者的伺服器處理其請求(若已有足夠座位釋出)

預約成功時的處理流程

  1. 持有該預約的伺服器向所有持有該場次等待使用者的伺服器發送訊息,讓其過期所有需要座位數超過可用座位數的等待使用者
  2. 收到訊息的伺服器查詢資料庫確認目前剩餘可用座位數(資料庫快取可大幅加速此查詢)
  3. 遍歷 Linked HashMap,過期所有需求座位數超過可用數量的等待使用者