當前位置: 妍妍網 > 碼農

秒沒售罄?!票務系統架構原來是這樣設計的……

2024-03-17碼農

作者介紹

MobotSton e 某AI大廠高級系統架構師,從事教育領域系統架構設計,ToG方向。

隨著資訊科技和互聯網的發展,票務系統也在不斷升級,比如實作了移動支付、電子票據、即時數據分析等先進功能。此外,許多票務系統還引入了人工智慧和大數據技術,用於精準行銷、個人化推薦和風險管理。

然而,票務系統也存在一些挑戰,如何保護使用者私密,如何防止票務欺詐,以及如何提供更好的使用者體驗等。因此,票務系統的開發和營運需要考慮到這些問題,並持續改進和升級。

計畫簡介:大麥網是中國的領先線上票務平台,提供多樣化的活動票務,如音樂會、戲劇和體育賽事等。主要功能包括活動搜尋、線上購票、電子票務、即時座位選擇、退換票服務以及支付介面。其智慧推薦系統可以根據使用者興趣推播相關活動,為使用者提供方便、快捷的一站式購票體驗。

類似的產品有:貓眼娛樂、永樂票務、bookmyshow.com、ticketmaster.com

難度級別:困難

一、什麽是線上電影票預訂系統

電影票預訂系統為其客戶提供線上購買影院座位的能力。電子票務系統允許客戶瀏覽當前正在上映的電影,並在任何地方任何時候預訂座位。

二、系統的需求和目標

我們的票務預訂服務應滿足以下需求:

功能需求:

  • 我們的票務預訂服務應能列出其聯盟影院所在的不同城市。

  • 使用者選擇城市後,服務應顯示該特定城市已經上映的電影。

  • 使用者選擇電影後,服務應顯示正在放映該電影的影院及其可用的放映時間。

  • 使用者應能選擇在特定影院的一場放映並預訂他們的票。

  • 服務應能向使用者展示影院大廳的座位布局。使用者應能根據他們的喜好選擇多個座位。

  • 使用者應能從已預訂的座位中區分出可用的座位。

  • 使用者應能在付款以完成預訂之前,將座位保留五分鐘。

  • 如果有可能座位會變得可用,例如,當其他使用者的保留到期時,使用者應能等待。

  • 等待的客戶應以公平的、先到先得的方式服務。

  • 非功能性需求:

  • 系統需要具有高度並行性。在任何特定時間點,都會有多個對同一座位的預訂請求。服務應能優雅且公平地處理這一情況。

  • 服務的核心是票務預訂,也就意味著涉及到財務交易。這意味著系統應具有安全性,並且資料庫應遵守ACID(原子性、一致性、隔離性、永續性)原則。

  • 三、一些設計考慮

  • 為了簡便,我們假設我們的服務不需要任何使用者認證。

  • 系統將 不處理部份票務訂單 。使用者要麽獲得他們想要的所有票,要麽一張也得不到。系統必須公平。

  • 為了 阻止系統被濫用 ,我們可以限制使用者一次預訂不超過十個座位。

  • 我們可以假設在熱門/備受期待的電影上映時,流量會激增,座位會很快被預訂完。

  • 系統應具有 可延伸性和高可用性 ,以應對流量激增。

  • 四、容量估計

  • 流量估計: 我們假設我們的服務每月有30億次頁面瀏覽,每月售出1000萬張電影票。

  • 儲存估計: 假設我 們有500個城市,平均每個城市有10家影院。如果每個影院有2000個座位,平均每天有兩場放映。

  • 我們假設每個座位預訂需要50字節(ID、NumberOfSeats、ShowID、MovieID、SeatNumbers、SeatStatus、Timestamp 等)儲存在資料庫中。我們還需要儲存關於電影和影院的資訊;我們假設它會需要50字節。所以,要儲存所有城市的所有影院的所有放映的所有數據一天:

    500個城市 * 10家影院 * 2000個座位 * 2場放映 * (50+50) 字節 = 2GB / 天

    要儲存五年的這些數據,我們大約需要3.6TB。

    五、系統API

    我們可以有SOAP或REST API來公開我們服務的功能。以下可能是搜尋電影放映和預訂座位的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" or "no"):是否在響應中包含拼寫檢查建議。

  • results_per_page (number):每頁返回的結果數。最大為30。

  • sort ing_order (string):搜尋結果的排序順序。一些可允許的值:'name,asc','name,desc','date,asc','date,desc','distance,asc','name,date,asc','name,date,desc','date,name,asc','date,name,desc'。

  • 返回: (JSON) 以下是電影及其放映的範例列表:

    {"MovieID": 1,"ShowID": 1,"Title": "Cars 2","Description": "About cars","Duration": 120,"Genre": "Animation","Language": "English","ReleaseDate": "8th Oct. 2014","Country": USA,"StartTime": "14:00","EndTime": "16:00","Seats": [ { "Type": "Regular""Price": 14.99"Status: "Almost Full" }, { "Type": "Premium" "Price": 24.99 "Status: "Available" }]},{"MovieID": 1,"ShowID": 2,"Title": "Cars 2","Description": "About cars","Duration": 120,"Genre": "Animation","Language": "English","ReleaseDate": "8th Oct. 2014","Country": USA,"StartTime": "16:30","EndTime": "18:30","Seats": [ { "Type": "Regular""Price": 14.99"Status: "Full" }, { "Type": "Premium" "Price": 24.99 "Status: "Almost Full" }]}

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

    參數:

  • api_dev_key (string):與上面相同

  • session_id (string):使用者的會話ID,用於跟蹤此預訂。一旦預訂時間到期,將使用此ID在伺服器上刪除使用者的預訂。

  • movie_id (string):預訂的電影。

  • show_id (string):預訂的放映。

  • seats_to_reserve (number):包含要預訂的座位ID的陣列。

  • 返回: (JSON)

    返回預訂的狀態,其中包括以下之一:1) "預訂成功" 2) "預訂失敗 - 放映已滿",3) "預訂失敗 - 請重試,因為其他使用者正在保留預訂座位"。

    六、資料庫設計

    以下是我們即將儲存的數據的一些觀察:

  • 每個城市可以有多個影院。

  • 每個影院將有多個影廳。

  • 每部電影將有多場放映,每場放映將有多次預訂。

  • 一個使用者可以有多次預訂。

  • 七、頂層設計

    在頂層面上,我們的web伺服器將管理使用者的會話,套用伺服器將處理所有的票務管理,將數據儲存在資料庫中,以及與緩存伺服器一起處理預訂。

    八、元件設計

    首先,我們試著建立服務,假設它是由一個單一的伺服器提供的。

    以下將是典型的票務預訂流程:

    1. 使用者搜尋一部電影。

    2. 使用者選擇一部電影。

    3. 向使用者顯示該電影的可用場次。

    4. 使用者選擇一場放映。

    5. 使用者選擇要預訂的座位數量。

    6. 如果需要的座位數可用,使用者將看到一個劇院的地圖以選擇座位。如果不是,使用者將進入下面的「步驟8」。

    7. 一旦使用者選擇了座位,系統將嘗試預訂這些選定的座位。

    8. 如果 無法預訂座位 ,我們有以下選項:

  • 放映已滿;向使用者顯示錯誤訊息。

  • 使用者想預訂的座位已經沒有了,但是還有其他座位可用,所以使用者被帶回到劇院地圖頁面以選擇不同的座位。

  • 沒有可預訂的座位,但所有座位都還沒有被預訂,因為有些座位被其他使用者在預訂池中保留並且還沒有預訂。使用者將被帶到一個等待頁面,在那裏他們可以等待直到需要的座位從預訂池中釋放。這個 等待 可能會導致以下選項:

  • 如果需要的座位數變得可用,使用者將被帶到劇院地圖頁面,他們可以選擇座位。

  • 在等待過程中,如果所有座位都被預訂了,或者預訂池中的座位數少於使用者打算預訂的座位數,使用者將被顯示錯誤訊息。

  • 使用者取消等待,返回到電影搜尋頁面。

  • 最多,使用者可以等待一個小時,之後使用者的會話將過期,使用者將被帶回到電影搜尋頁面。

  • 如果成功預訂了座位,使用者有五分鐘的時間支付預訂。付款後,預訂標記為完成。如果使用者不能在五分鐘內支付,他們所有的預訂座位都將被釋放,以供其他使用者使用。

    伺服器如何跟蹤所有尚未預訂的活動預訂?伺服器又如何跟蹤所有等待的客戶?

    我們需要 兩個守護服務 ,一個用來跟蹤所有活動的預訂並從系統中移除任何過期的預訂;我們稱之為 ActiveReservationService 。另一個服務將跟蹤所有等待的使用者請求,一旦需要的座位數變得可用,它將通知(等待時間最長的)使用者選擇座位;我們稱之為 WaitingUserService

    1. ActiveReservationsService(活動預訂服務)

    我們可以在記憶體中保留一個與Linked HashMap或TreeMap相似的數據結構來儲存一場「演出」的所有預訂,除了在資料庫中保留所有數據。我們需要一種Linked HashMap型別的數據結構,它允許我們在預訂完成時跳轉到任何預訂以移除它。此外,由於我們將有與每個預訂關聯的到期時間,HashMap的頭部將始終指向最舊的預訂記錄,以便在達到超時時過期預訂。

    為了儲存每場演出的每個預訂,我們可以有一個HashTable,其中'key'是'ShowID','value'是包含'BookingID'和建立'Timestamp'的Linked HashMap。

    在資料庫中,我們將在'Booking'表中儲存預訂,到期時間將在Timestamp列中。'Status'欄位將有一個值為'Reserved (1)'的值,一旦預訂完成,系統將更新'Status'為'Booked (2)'並從相關演出的Linked HashMap中刪除預訂記錄。當預訂過期時,我們可以從Booking表中移除它,或者將其標記為'Expired (3)',除此之外還要從記憶體中移除。

    ActiveReservationsService也將與外部金融服務一起處理使用者支付。每當預訂完成或預訂過期時,WaitingUsersService都會收到一個訊號,以便可以為任何等待的客戶提供服務。

    2. WaitingUsersService(等待使用者服務)

    就像ActiveReservationsService一樣,我們可以將一個演出的所有等待使用者儲存在Linked HashMap或TreeMap的記憶體中。我們需要一個類似於Linked HashMap的數據結構,以便我們可以在使用者取消請求時跳轉到任何使用者以從HashMap中移除他們。此外,由於我們是以先到先得的方式服務,Linked HashMap的頭部總是指向等待時間最長的使用者,因此每當座位變得可用時,我們都可以以公平的方式為使用者提供服務。

    我們將有一個HashTable用來儲存每個Show的所有等待使用者。'key'將是'ShowID','value'將是包含'UserIDs'和他們的等待開始時間的Linked HashMap。

    客戶端可以使用Long Polling來保持自己的預訂狀態更新。每當座位變得可用時,伺服器可以使用這個請求來通知使用者。

    預訂過期

    在伺服器上,ActiveReservationsService跟蹤活動預訂的過期時間(基於預訂時間)。由於客戶端將顯示一個計時器(用於過期時間),這可能與伺服器稍微不同步,我們可以在伺服器上添加五秒鐘的緩沖區以防止破碎的體驗,從而確保客戶端在伺服器超時後永不超時,防止成功購買。

    九、並行性

    如何處理並行性,以便沒有兩個使用者能夠預訂同一座位。我們可以在SQL資料庫中使用事務來避免任何沖突。例如,如果我們使用的是SQL伺服器,我們可以利用事務隔離級別來釘選行,然後再更新它們。下面是樣本程式碼:

    SETTRANSACTIONISOLATIONLEVELSERIALIZABLE;BEGINTRANSACTION;-- Suppose we intend to reserve three seats (IDs: 54, 55, 56) for ShowID=99 Select * From Show_Seat where ShowID=99 && ShowSeatID in (54, 55, 56) && Status=0-- free -- if the number of rows returned by the above statement is three, we can update to -- return success otherwise return failure to the user.update Show_Seat ...update Booking ...COMMITTRANSACTION;

    'Serializable' 是最高的隔離級別,可以保證免受臟讀、不可重復讀和幻讀的影響。這裏要註意一點;在一個事務中,如果我們讀取了行,我們會在這些行上加寫鎖,以防止它們被任何其他人更新。

    一旦上述資料庫事務成功,我們就可以開始在ActiveReservationService中跟蹤預訂情況。

    十、容錯性

    當ActiveReservationsService或WaitingUsersService崩潰時會發生什麽?每當ActiveReservationsService崩潰時,我們可以從‘Booking’表中讀取所有的活動預訂。請記住,直到預訂完成,我們都將「Status」列保持為「Reserved (1)」。另一個選擇是擁有主-次配置,這樣,當主服務崩潰時,次服務可以接管。我們沒有將等待的使用者儲存在資料庫中,所以,當WaitingUsersService崩潰時,除非我們有主次設定,否則我們沒有任何方式恢復那些數據。

    同樣,我們會為資料庫設定主次配置,以使其具有容錯性。

    十一、數據分區

  • 資料庫分區: 如果我們按‘MovieID’進行分區,那麽一部電影的所有場次都會在同一個伺服器上。對於熱門電影來說,這可能會給那台伺服器帶來大量負載。更好的方法是根據ShowID進行分區;這樣,負載就可以分散到不同的伺服器上。

  • ActiveReservationService 和 WaitingUserService 分區: 我們的Web伺服器將管理所有活動使用者的會話,並處理與使用者的所有通訊。我們可以使用一致性哈希演算法來根據‘ShowID’為ActiveReservationService和WaitingUserService分配套用伺服器。這樣,特定場次的所有預訂和等待使用者將由某一組伺服器處理。假設為了負載平衡,我們的"一致性哈希"為任何場次分配了三個伺服器,那麽每當一個預訂過期時,持有該預訂的伺服器將執行以下操作:

  • 如果需要的座位數變得可用,使用者將被帶到劇院地圖頁面,他們可以選擇座位。

  • 更新資料庫以移除預訂(或標記為過期)並更新‘Show_Seats’表中座位的狀態。

  • 從Linked HashMap中移除預訂。

  • 通知使用者他們的預訂已過期。

  • 向所有持有該場次等待使用者的WaitingUserService伺服器廣播訊息,以找出等待時間最長的使用者。一致性哈希方案將告訴我們哪些伺服器持有這些使用者。

  • 如果所需的座位已經變為可用,就向持有最長等待使用者的WaitingUserService伺服器發送訊息以處理他們的請求。

  • 每當一個預訂成功時,將發生以下事情:

  • 持有該預訂的伺服器向所有持有該場次等待使用者的伺服器發送訊息,以便這些伺服器可以使所有需要的座位數多於可用座位數的等待使用者過期。

  • 收到上述訊息後,所有持有等待使用者的伺服器將查詢資料庫,以尋找現在有多少個空閑座位。此處的資料庫緩存將大大有助於只執行一次這個查詢。

  • 使所有希望預訂的座位數多於可用座位數的等待使用者過期。為此,WaitingUserService必須遍歷所有等待使用者的Linked HashMap。

  • 源丨juejin.cn/post/7252684331712692284

    dbaplus社群歡迎廣大技術人員投稿,投稿信箱: [email protected]