經典系統架構設計案例分析:票務系統深度解析
隨著信息技術和互聯網的發展,票務系統也在不斷升級,比如實現了移動支付、電子票據、實時數據分析等先進功能。此外,許多票務系統還引入了人工智能和大數據技術,用于精準營銷、個性化推薦和風險管理。
然而,票務系統也存在一些挑戰,如如何保護用戶隱私,如何防止票務欺詐,以及如何提供更好的用戶體驗等。因此,票務系統的開發和運營需要考慮到這些問題,并持續改進和升級。
項目簡介:大麥網是中國的領先在線票務平臺,提供多樣化的活動票務,如音樂會、戲劇和體育賽事等。主要功能包括活動搜索、在線購票、電子票務、實時座位選擇、退換票服務以及支付接口。其智能推薦系統可以根據用戶興趣推送相關活動,為用戶提供方便、快捷的一站式購票體驗。
類似的產品有:貓眼娛樂、永樂票務、bookmyshow.com、ticketmaster.com
難度級別:困難
1、什么是在線電影票預訂系統
電影票預訂系統為其客戶提供在線購買影院座位的能力。電子票務系統允許客戶瀏覽當前正在上映的電影,并在任何地方任何時候預訂座位。
2、系統的需求和目標
我們的票務預訂服務應滿足以下需求:
功能需求:
- 我們的票務預訂服務應能列出其聯盟影院所在的不同城市。
- 用戶選擇城市后,服務應顯示該特定城市已經上映的電影。
- 用戶選擇電影后,服務應顯示正在放映該電影的影院及其可用的放映時間。
- 用戶應能選擇在特定影院的一場放映并預訂他們的票。
- 服務應能向用戶展示影院大廳的座位布局。用戶應能根據他們的喜好選擇多個座位。
- 用戶應能從已預訂的座位中區分出可用的座位。
- 用戶應能在付款以完成預訂之前,將座位保留五分鐘。
- 如果有可能座位會變得可用,例如,當其他用戶的保留到期時,用戶應能等待。
- 等待的客戶應以公平的、先到先得的方式服務。
非功能性需求:
- 系統需要具有高度并發性。在任何特定時間點,都會有多個對同一座位的預訂請求。服務應能優雅且公平地處理這一情況。
- 服務的核心是票務預訂,也就意味著涉及到財務交易。這意味著系統應具有安全性,并且數據庫應遵守ACID(原子性、一致性、隔離性、持久性)原則。
3、一些設計考慮
- 為了簡便,我們假設我們的服務不需要任何用戶認證。
- 系統將不處理部分票務訂單。用戶要么獲得他們想要的所有票,要么一張也得不到。 系統必須公平。
- 為了阻止系統被濫用,我們可以限制用戶一次預訂不超過十個座位。
- 我們可以假設在熱門/備受期待的電影上映時,流量會激增,座位會很快被預訂完。
- 系統應具有可擴展性和高可用性,以應對流量激增。
4、容量估計
流量估計:我們假設我們的服務每月有30億次頁面瀏覽,每月售出1000萬張電影票。
存儲估計:假設我們有500個城市,平均每個城市有10家影院。如果每個影院有2000個座位,平均每天有兩場放映。
我們假設每個座位預訂需要50字節(ID、NumberOfSeats、ShowID、MovieID、SeatNumbers、SeatStatus、Timestamp 等)存儲在數據庫中。我們還需要存儲關于電影和影院的信息;我們假設它會需要50字節。所以,要存儲所有城市的所有影院的所有放映的所有數據一天:
500個城市 * 10家影院 * 2000個座位 * 2場放映 * (50+50) 字節 = 2GB / 天
要存儲五年的這些數據,我們大約需要3.6TB。
5、系統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。
- sorting_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) "預訂失敗 - 請重試,因為其他用戶正在保留預訂座位"。
6、數據庫設計
以下是我們即將存儲的數據的一些觀察:
- 每個城市可以有多個影院。
- 每個影院將有多個影廳。
- 每部電影將有多場放映,每場放映將有多次預訂。
- 一個用戶可以有多次預訂。
7、頂層設計
在頂層面上,我們的web服務器將管理用戶的會話,應用服務器將處理所有的票務管理,將數據存儲在數據庫中,以及與緩存服務器一起處理預訂。
8、組件設計
首先,我們試著建立服務,假設它是由一個單一的服務器提供的。
票務預訂流程:以下將是典型的票務預訂流程:
- 用戶搜索一部電影。
- 用戶選擇一部電影。
- 向用戶顯示該電影的可用場次。
- 用戶選擇一場放映。
- 用戶選擇要預訂的座位數量。
- 如果需要的座位數可用,用戶將看到一個劇院的地圖以選擇座位。如果不是,用戶將進入下面的“步驟8”。
- 一旦用戶選擇了座位,系統將嘗試預訂這些選定的座位。
- 如果無法預訂座位,我們有以下選項:
- 放映已滿;向用戶顯示錯誤消息。
- 用戶想預訂的座位已經沒有了,但是還有其他座位可用,所以用戶被帶回到劇院地圖頁面以選擇不同的座位。
- 沒有可預訂的座位,但所有座位都還沒有被預訂,因為有些座位被其他用戶在預訂池中保留并且還沒有預訂。用戶將被帶到一個等待頁面,在那里他們可以等待直到需要的座位從預訂池中釋放。這個等待可能會導致以下選項:
- 如果需要的座位數變得可用,用戶將被帶到劇院地圖頁面,他們可以選擇座位。
- 在等待過程中,如果所有座位都被預訂了,或者預訂池中的座位數少于用戶打算預訂的座位數,用戶將被顯示錯誤消息。
- 用戶取消等待,返回到電影搜索頁面。
- 最多,用戶可以等待一個小時,之后用戶的會話將過期,用戶將被帶回到電影搜索頁面。
- 如果成功預訂了座位,用戶有五分鐘的時間支付預訂。付款后,預訂標記為完成。如果用戶不能在五分鐘內支付,他們所有的預訂座位都將被釋放,以供其他用戶使用。
服務器如何跟蹤所有尚未預訂的活動預訂?服務器又如何跟蹤所有等待的客戶? 我們需要兩個守護服務,一個用來跟蹤所有活動的預訂并從系統中移除任何過期的預訂;我們稱之為ActiveReservationService。另一個服務將跟蹤所有等待的用戶請求,一旦需要的座位數變得可用,它將通知(等待時間最長的)用戶選擇座位;我們稱之為WaitingUserService。
A. 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都會收到一個信號,以便可以為任何等待的客戶提供服務。
B. WaitingUsersService(等待用戶服務)
就像ActiveReservationsService一樣,我們可以將一個演出的所有等待用戶存儲在Linked HashMap或TreeMap的內存中。我們需要一個類似于Linked HashMap的數據結構,以便我們可以在用戶取消請求時跳轉到任何用戶以從HashMap中移除他們。此外,由于我們是以先到先得的方式服務,Linked HashMap的頭部總是指向等待時間最長的用戶,因此每當座位變得可用時,我們都可以以公平的方式為用戶提供服務。
我們將有一個HashTable用來存儲每個Show的所有等待用戶。'key'將是'ShowID','value'將是包含'UserIDs'和他們的等待開始時間的Linked HashMap。
客戶端可以使用Long Polling來保持自己的預訂狀態更新。每當座位變得可用時,服務器可以使用這個請求來通知用戶。
預訂過期
在服務器上,ActiveReservationsService跟蹤活動預訂的過期時間(基于預訂時間)。由于客戶端將顯示一個計時器(用于過期時間),這可能與服務器稍微不同步,我們可以在服務器上添加五秒鐘的緩沖區以防止破碎的體驗,從而確??蛻舳嗽诜掌鞒瑫r后永不超時,防止成功購買。
9、并發性
如何處理并發性,以便沒有兩個用戶能夠預訂同一座位。我們可以在SQL數據庫中使用事務來避免任何沖突。例如,如果我們使用的是SQL服務器,我們可以利用事務隔離級別來鎖定行,然后再更新它們。下面是樣本代碼:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRANSACTION;
-- 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 ...
COMMIT TRANSACTION;
'Serializable' 是最高的隔離級別,可以保證免受臟讀、不可重復讀和幻讀的影響。這里要注意一點;在一個事務中,如果我們讀取了行,我們會在這些行上加寫鎖,以防止它們被任何其他人更新。
一旦上述數據庫事務成功,我們就可以開始在ActiveReservationService中跟蹤預訂情況。
10、容錯性
當ActiveReservationsService或WaitingUsersService崩潰時會發生什么? 每當ActiveReservationsService崩潰時,我們可以從‘Booking’表中讀取所有的活動預訂。請記住,直到預訂完成,我們都將“Status”列保持為“Reserved (1)”。另一個選擇是擁有主-次配置,這樣,當主服務崩潰時,次服務可以接管。我們沒有將等待的用戶存儲在數據庫中,所以,當WaitingUsersService崩潰時,除非我們有主次設置,否則我們沒有任何方式恢復那些數據。
同樣,我們會為數據庫設置主次配置,以使其具有容錯性。
11、數據分區
數據庫分區:如果我們按‘MovieID’進行分區,那么一部電影的所有場次都會在同一個服務器上。對于熱門電影來說,這可能會給那臺服務器帶來大量負載。更好的方法是根據ShowID進行分區;這樣,負載就可以分散到不同的服務器上。
ActiveReservationService和WaitingUserService分區:我們的Web服務器將管理所有活動用戶的會話,并處理與用戶的所有通信。我們可以使用一致性哈希算法來根據‘ShowID’為ActiveReservationService和WaitingUserService分配應用服務器。這樣,特定場次的所有預訂和等待用戶將由某一組服務器處理。假設為了負載平衡,我們的"一致性哈希"為任何場次分配了三個服務器,那么每當一個預訂過期時,持有該預訂的服務器將執行以下操作:
- 更新數據庫以移除預訂(或標記為過期)并更新‘Show_Seats’表中座位的狀態。
- 從Linked HashMap中移除預訂。
- 通知用戶他們的預訂已過期。
- 向所有持有該場次等待用戶的WaitingUserService服務器廣播消息,以找出等待時間最長的用戶。一致性哈希方案將告訴我們哪些服務器持有這些用戶。
- 如果所需的座位已經變為可用,就向持有最長等待用戶的WaitingUserService服務器發送消息以處理他們的請求。
每當一個預訂成功時,將發生以下事情:
- 持有該預訂的服務器向所有持有該場次等待用戶的服務器發送消息,以便這些服務器可以使所有需要的座位數多于可用座位數的等待用戶過期。
- 收到上述消息后,所有持有等待用戶的服務器將查詢數據庫,以查找現在有多少個空閑座位。此處的數據庫緩存將大大有助于只運行一次這個查詢。
- 使所有希望預訂的座位數多于可用座位數的等待用戶過期。為此,WaitingUserService必須遍歷所有等待用戶的Linked HashMap。