經(jīng)典系統(tǒng)架構(gòu)設(shè)計(jì)案例分析:票務(wù)系統(tǒng)深度解析
隨著信息技術(shù)和互聯(lián)網(wǎng)的發(fā)展,票務(wù)系統(tǒng)也在不斷升級(jí),比如實(shí)現(xiàn)了移動(dòng)支付、電子票據(jù)、實(shí)時(shí)數(shù)據(jù)分析等先進(jìn)功能。此外,許多票務(wù)系統(tǒng)還引入了人工智能和大數(shù)據(jù)技術(shù),用于精準(zhǔn)營(yíng)銷(xiāo)、個(gè)性化推薦和風(fēng)險(xiǎn)管理。
然而,票務(wù)系統(tǒng)也存在一些挑戰(zhàn),如如何保護(hù)用戶(hù)隱私,如何防止票務(wù)欺詐,以及如何提供更好的用戶(hù)體驗(yàn)等。因此,票務(wù)系統(tǒng)的開(kāi)發(fā)和運(yùn)營(yíng)需要考慮到這些問(wèn)題,并持續(xù)改進(jìn)和升級(jí)。
項(xiàng)目簡(jiǎn)介:大麥網(wǎng)是中國(guó)的領(lǐng)先在線(xiàn)票務(wù)平臺(tái),提供多樣化的活動(dòng)票務(wù),如音樂(lè)會(huì)、戲劇和體育賽事等。主要功能包括活動(dòng)搜索、在線(xiàn)購(gòu)票、電子票務(wù)、實(shí)時(shí)座位選擇、退換票服務(wù)以及支付接口。其智能推薦系統(tǒng)可以根據(jù)用戶(hù)興趣推送相關(guān)活動(dòng),為用戶(hù)提供方便、快捷的一站式購(gòu)票體驗(yàn)。
類(lèi)似的產(chǎn)品有:貓眼娛樂(lè)、永樂(lè)票務(wù)、bookmyshow.com、ticketmaster.com
難度級(jí)別:困難
1、什么是在線(xiàn)電影票預(yù)訂系統(tǒng)
電影票預(yù)訂系統(tǒng)為其客戶(hù)提供在線(xiàn)購(gòu)買(mǎi)影院座位的能力。電子票務(wù)系統(tǒng)允許客戶(hù)瀏覽當(dāng)前正在上映的電影,并在任何地方任何時(shí)候預(yù)訂座位。
2、系統(tǒng)的需求和目標(biāo)
我們的票務(wù)預(yù)訂服務(wù)應(yīng)滿(mǎn)足以下需求:
功能需求:
- 我們的票務(wù)預(yù)訂服務(wù)應(yīng)能列出其聯(lián)盟影院所在的不同城市。
- 用戶(hù)選擇城市后,服務(wù)應(yīng)顯示該特定城市已經(jīng)上映的電影。
- 用戶(hù)選擇電影后,服務(wù)應(yīng)顯示正在放映該電影的影院及其可用的放映時(shí)間。
- 用戶(hù)應(yīng)能選擇在特定影院的一場(chǎng)放映并預(yù)訂他們的票。
- 服務(wù)應(yīng)能向用戶(hù)展示影院大廳的座位布局。用戶(hù)應(yīng)能根據(jù)他們的喜好選擇多個(gè)座位。
- 用戶(hù)應(yīng)能從已預(yù)訂的座位中區(qū)分出可用的座位。
- 用戶(hù)應(yīng)能在付款以完成預(yù)訂之前,將座位保留五分鐘。
- 如果有可能座位會(huì)變得可用,例如,當(dāng)其他用戶(hù)的保留到期時(shí),用戶(hù)應(yīng)能等待。
- 等待的客戶(hù)應(yīng)以公平的、先到先得的方式服務(wù)。
非功能性需求:
- 系統(tǒng)需要具有高度并發(fā)性。在任何特定時(shí)間點(diǎn),都會(huì)有多個(gè)對(duì)同一座位的預(yù)訂請(qǐng)求。服務(wù)應(yīng)能優(yōu)雅且公平地處理這一情況。
- 服務(wù)的核心是票務(wù)預(yù)訂,也就意味著涉及到財(cái)務(wù)交易。這意味著系統(tǒng)應(yīng)具有安全性,并且數(shù)據(jù)庫(kù)應(yīng)遵守ACID(原子性、一致性、隔離性、持久性)原則。
3、一些設(shè)計(jì)考慮
- 為了簡(jiǎn)便,我們假設(shè)我們的服務(wù)不需要任何用戶(hù)認(rèn)證。
- 系統(tǒng)將不處理部分票務(wù)訂單。用戶(hù)要么獲得他們想要的所有票,要么一張也得不到。 系統(tǒng)必須公平。
- 為了阻止系統(tǒng)被濫用,我們可以限制用戶(hù)一次預(yù)訂不超過(guò)十個(gè)座位。
- 我們可以假設(shè)在熱門(mén)/備受期待的電影上映時(shí),流量會(huì)激增,座位會(huì)很快被預(yù)訂完。
- 系統(tǒng)應(yīng)具有可擴(kuò)展性和高可用性,以應(yīng)對(duì)流量激增。
4、容量估計(jì)
流量估計(jì):我們假設(shè)我們的服務(wù)每月有30億次頁(yè)面瀏覽,每月售出1000萬(wàn)張電影票。
存儲(chǔ)估計(jì):假設(shè)我們有500個(gè)城市,平均每個(gè)城市有10家影院。如果每個(gè)影院有2000個(gè)座位,平均每天有兩場(chǎng)放映。
我們假設(shè)每個(gè)座位預(yù)訂需要50字節(jié)(ID、NumberOfSeats、ShowID、MovieID、SeatNumbers、SeatStatus、Timestamp 等)存儲(chǔ)在數(shù)據(jù)庫(kù)中。我們還需要存儲(chǔ)關(guān)于電影和影院的信息;我們假設(shè)它會(huì)需要50字節(jié)。所以,要存儲(chǔ)所有城市的所有影院的所有放映的所有數(shù)據(jù)一天:
500個(gè)城市 * 10家影院 * 2000個(gè)座位 * 2場(chǎng)放映 * (50+50) 字節(jié) = 2GB / 天
要存儲(chǔ)五年的這些數(shù)據(jù),我們大約需要3.6TB。
5、系統(tǒng)API
我們可以有SOAP或REST API來(lái)公開(kāi)我們服務(wù)的功能。以下可能是搜索電影放映和預(yù)訂座位的API的定義。
SearchMovies(api_dev_key, keyword, city, lat_long, radius, start_datetime, end_datetime, postal_code, includeSpellcheck, results_per_page, sorting_order)
參數(shù):
- api_dev_key (string):注冊(cè)賬戶(hù)的API開(kāi)發(fā)者密鑰。這將用于包括限制用戶(hù)基于其分配的配額等在內(nèi)的事情。
- keyword (string):要搜索的關(guān)鍵詞。
- city (string):用于篩選電影的城市。
- lat_long (string):用于篩選的緯度和經(jīng)度。
- radius (number):我們想要搜索活動(dòng)的區(qū)域的半徑。
- start_datetime (string):用開(kāi)始日期時(shí)間篩選電影。
- end_datetime (string):用結(jié)束日期時(shí)間篩選電影。
- postal_code (string):用郵政編碼/郵編篩選電影。
- includeSpellcheck (Enum: "yes" or "no"):是否在響應(yīng)中包含拼寫(xiě)檢查建議。
- results_per_page (number):每頁(yè)返回的結(jié)果數(shù)。最大為30。
- sorting_order (string):搜索結(jié)果的排序順序。一些可允許的值:'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[])
參數(shù):
- api_dev_key (string):與上面相同
- session_id (string):用戶(hù)的會(huì)話(huà)ID,用于跟蹤此預(yù)訂。一旦預(yù)訂時(shí)間到期,將使用此ID在服務(wù)器上刪除用戶(hù)的預(yù)訂。
- movie_id (string):預(yù)訂的電影。
- show_id (string):預(yù)訂的放映。
- seats_to_reserve (number):包含要預(yù)訂的座位ID的數(shù)組。
返回:(JSON)
返回預(yù)訂的狀態(tài),其中包括以下之一:
- 1) "預(yù)訂成功"
- 2) "預(yù)訂失敗 - 放映已滿(mǎn)",
- 3) "預(yù)訂失敗 - 請(qǐng)重試,因?yàn)槠渌脩?hù)正在保留預(yù)訂座位"。
6、數(shù)據(jù)庫(kù)設(shè)計(jì)
以下是我們即將存儲(chǔ)的數(shù)據(jù)的一些觀(guān)察:
- 每個(gè)城市可以有多個(gè)影院。
- 每個(gè)影院將有多個(gè)影廳。
- 每部電影將有多場(chǎng)放映,每場(chǎng)放映將有多次預(yù)訂。
- 一個(gè)用戶(hù)可以有多次預(yù)訂。
7、頂層設(shè)計(jì)
在頂層面上,我們的web服務(wù)器將管理用戶(hù)的會(huì)話(huà),應(yīng)用服務(wù)器將處理所有的票務(wù)管理,將數(shù)據(jù)存儲(chǔ)在數(shù)據(jù)庫(kù)中,以及與緩存服務(wù)器一起處理預(yù)訂。
8、組件設(shè)計(jì)
首先,我們?cè)囍⒎?wù),假設(shè)它是由一個(gè)單一的服務(wù)器提供的。
票務(wù)預(yù)訂流程:以下將是典型的票務(wù)預(yù)訂流程:
- 用戶(hù)搜索一部電影。
- 用戶(hù)選擇一部電影。
- 向用戶(hù)顯示該電影的可用場(chǎng)次。
- 用戶(hù)選擇一場(chǎng)放映。
- 用戶(hù)選擇要預(yù)訂的座位數(shù)量。
- 如果需要的座位數(shù)可用,用戶(hù)將看到一個(gè)劇院的地圖以選擇座位。如果不是,用戶(hù)將進(jìn)入下面的“步驟8”。
- 一旦用戶(hù)選擇了座位,系統(tǒng)將嘗試預(yù)訂這些選定的座位。
- 如果無(wú)法預(yù)訂座位,我們有以下選項(xiàng):
- 放映已滿(mǎn);向用戶(hù)顯示錯(cuò)誤消息。
- 用戶(hù)想預(yù)訂的座位已經(jīng)沒(méi)有了,但是還有其他座位可用,所以用戶(hù)被帶回到劇院地圖頁(yè)面以選擇不同的座位。
- 沒(méi)有可預(yù)訂的座位,但所有座位都還沒(méi)有被預(yù)訂,因?yàn)橛行┳槐黄渌脩?hù)在預(yù)訂池中保留并且還沒(méi)有預(yù)訂。用戶(hù)將被帶到一個(gè)等待頁(yè)面,在那里他們可以等待直到需要的座位從預(yù)訂池中釋放。這個(gè)等待可能會(huì)導(dǎo)致以下選項(xiàng):
- 如果需要的座位數(shù)變得可用,用戶(hù)將被帶到劇院地圖頁(yè)面,他們可以選擇座位。
- 在等待過(guò)程中,如果所有座位都被預(yù)訂了,或者預(yù)訂池中的座位數(shù)少于用戶(hù)打算預(yù)訂的座位數(shù),用戶(hù)將被顯示錯(cuò)誤消息。
- 用戶(hù)取消等待,返回到電影搜索頁(yè)面。
- 最多,用戶(hù)可以等待一個(gè)小時(shí),之后用戶(hù)的會(huì)話(huà)將過(guò)期,用戶(hù)將被帶回到電影搜索頁(yè)面。
- 如果成功預(yù)訂了座位,用戶(hù)有五分鐘的時(shí)間支付預(yù)訂。付款后,預(yù)訂標(biāo)記為完成。如果用戶(hù)不能在五分鐘內(nèi)支付,他們所有的預(yù)訂座位都將被釋放,以供其他用戶(hù)使用。
服務(wù)器如何跟蹤所有尚未預(yù)訂的活動(dòng)預(yù)訂?服務(wù)器又如何跟蹤所有等待的客戶(hù)? 我們需要兩個(gè)守護(hù)服務(wù),一個(gè)用來(lái)跟蹤所有活動(dòng)的預(yù)訂并從系統(tǒng)中移除任何過(guò)期的預(yù)訂;我們稱(chēng)之為ActiveReservationService。另一個(gè)服務(wù)將跟蹤所有等待的用戶(hù)請(qǐng)求,一旦需要的座位數(shù)變得可用,它將通知(等待時(shí)間最長(zhǎng)的)用戶(hù)選擇座位;我們稱(chēng)之為WaitingUserService。
A. ActiveReservationsService(活動(dòng)預(yù)訂服務(wù))
我們可以在內(nèi)存中保留一個(gè)與Linked HashMap或TreeMap相似的數(shù)據(jù)結(jié)構(gòu)來(lái)存儲(chǔ)一場(chǎng)“演出”的所有預(yù)訂,除了在數(shù)據(jù)庫(kù)中保留所有數(shù)據(jù)。我們需要一種Linked HashMap類(lèi)型的數(shù)據(jù)結(jié)構(gòu),它允許我們?cè)陬A(yù)訂完成時(shí)跳轉(zhuǎn)到任何預(yù)訂以移除它。此外,由于我們將有與每個(gè)預(yù)訂關(guān)聯(lián)的到期時(shí)間,HashMap的頭部將始終指向最舊的預(yù)訂記錄,以便在達(dá)到超時(shí)時(shí)過(guò)期預(yù)訂。
為了存儲(chǔ)每場(chǎng)演出的每個(gè)預(yù)訂,我們可以有一個(gè)HashTable,其中'key'是'ShowID','value'是包含'BookingID'和創(chuàng)建'Timestamp'的Linked HashMap。
在數(shù)據(jù)庫(kù)中,我們將在'Booking'表中存儲(chǔ)預(yù)訂,到期時(shí)間將在Timestamp列中。'Status'字段將有一個(gè)值為'Reserved (1)'的值,一旦預(yù)訂完成,系統(tǒng)將更新'Status'為'Booked (2)'并從相關(guān)演出的Linked HashMap中刪除預(yù)訂記錄。當(dāng)預(yù)訂過(guò)期時(shí),我們可以從Booking表中移除它,或者將其標(biāo)記為'Expired (3)',除此之外還要從內(nèi)存中移除。
ActiveReservationsService也將與外部金融服務(wù)一起處理用戶(hù)支付。每當(dāng)預(yù)訂完成或預(yù)訂過(guò)期時(shí),WaitingUsersService都會(huì)收到一個(gè)信號(hào),以便可以為任何等待的客戶(hù)提供服務(wù)。
B. WaitingUsersService(等待用戶(hù)服務(wù))
就像ActiveReservationsService一樣,我們可以將一個(gè)演出的所有等待用戶(hù)存儲(chǔ)在Linked HashMap或TreeMap的內(nèi)存中。我們需要一個(gè)類(lèi)似于Linked HashMap的數(shù)據(jù)結(jié)構(gòu),以便我們可以在用戶(hù)取消請(qǐng)求時(shí)跳轉(zhuǎn)到任何用戶(hù)以從HashMap中移除他們。此外,由于我們是以先到先得的方式服務(wù),Linked HashMap的頭部總是指向等待時(shí)間最長(zhǎng)的用戶(hù),因此每當(dāng)座位變得可用時(shí),我們都可以以公平的方式為用戶(hù)提供服務(wù)。
我們將有一個(gè)HashTable用來(lái)存儲(chǔ)每個(gè)Show的所有等待用戶(hù)。'key'將是'ShowID','value'將是包含'UserIDs'和他們的等待開(kāi)始時(shí)間的Linked HashMap。
客戶(hù)端可以使用Long Polling來(lái)保持自己的預(yù)訂狀態(tài)更新。每當(dāng)座位變得可用時(shí),服務(wù)器可以使用這個(gè)請(qǐng)求來(lái)通知用戶(hù)。
預(yù)訂過(guò)期
在服務(wù)器上,ActiveReservationsService跟蹤活動(dòng)預(yù)訂的過(guò)期時(shí)間(基于預(yù)訂時(shí)間)。由于客戶(hù)端將顯示一個(gè)計(jì)時(shí)器(用于過(guò)期時(shí)間),這可能與服務(wù)器稍微不同步,我們可以在服務(wù)器上添加五秒鐘的緩沖區(qū)以防止破碎的體驗(yàn),從而確??蛻?hù)端在服務(wù)器超時(shí)后永不超時(shí),防止成功購(gòu)買(mǎi)。
9、并發(fā)性
如何處理并發(fā)性,以便沒(méi)有兩個(gè)用戶(hù)能夠預(yù)訂同一座位。我們可以在SQL數(shù)據(jù)庫(kù)中使用事務(wù)來(lái)避免任何沖突。例如,如果我們使用的是SQL服務(wù)器,我們可以利用事務(wù)隔離級(jí)別來(lái)鎖定行,然后再更新它們。下面是樣本代碼:
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' 是最高的隔離級(jí)別,可以保證免受臟讀、不可重復(fù)讀和幻讀的影響。這里要注意一點(diǎn);在一個(gè)事務(wù)中,如果我們讀取了行,我們會(huì)在這些行上加寫(xiě)鎖,以防止它們被任何其他人更新。
一旦上述數(shù)據(jù)庫(kù)事務(wù)成功,我們就可以開(kāi)始在ActiveReservationService中跟蹤預(yù)訂情況。
10、容錯(cuò)性
當(dāng)ActiveReservationsService或WaitingUsersService崩潰時(shí)會(huì)發(fā)生什么? 每當(dāng)ActiveReservationsService崩潰時(shí),我們可以從‘Booking’表中讀取所有的活動(dòng)預(yù)訂。請(qǐng)記住,直到預(yù)訂完成,我們都將“Status”列保持為“Reserved (1)”。另一個(gè)選擇是擁有主-次配置,這樣,當(dāng)主服務(wù)崩潰時(shí),次服務(wù)可以接管。我們沒(méi)有將等待的用戶(hù)存儲(chǔ)在數(shù)據(jù)庫(kù)中,所以,當(dāng)WaitingUsersService崩潰時(shí),除非我們有主次設(shè)置,否則我們沒(méi)有任何方式恢復(fù)那些數(shù)據(jù)。
同樣,我們會(huì)為數(shù)據(jù)庫(kù)設(shè)置主次配置,以使其具有容錯(cuò)性。
11、數(shù)據(jù)分區(qū)
數(shù)據(jù)庫(kù)分區(qū):如果我們按‘MovieID’進(jìn)行分區(qū),那么一部電影的所有場(chǎng)次都會(huì)在同一個(gè)服務(wù)器上。對(duì)于熱門(mén)電影來(lái)說(shuō),這可能會(huì)給那臺(tái)服務(wù)器帶來(lái)大量負(fù)載。更好的方法是根據(jù)ShowID進(jìn)行分區(qū);這樣,負(fù)載就可以分散到不同的服務(wù)器上。
ActiveReservationService和WaitingUserService分區(qū):我們的Web服務(wù)器將管理所有活動(dòng)用戶(hù)的會(huì)話(huà),并處理與用戶(hù)的所有通信。我們可以使用一致性哈希算法來(lái)根據(jù)‘ShowID’為ActiveReservationService和WaitingUserService分配應(yīng)用服務(wù)器。這樣,特定場(chǎng)次的所有預(yù)訂和等待用戶(hù)將由某一組服務(wù)器處理。假設(shè)為了負(fù)載平衡,我們的"一致性哈希"為任何場(chǎng)次分配了三個(gè)服務(wù)器,那么每當(dāng)一個(gè)預(yù)訂過(guò)期時(shí),持有該預(yù)訂的服務(wù)器將執(zhí)行以下操作:
- 更新數(shù)據(jù)庫(kù)以移除預(yù)訂(或標(biāo)記為過(guò)期)并更新‘Show_Seats’表中座位的狀態(tài)。
- 從Linked HashMap中移除預(yù)訂。
- 通知用戶(hù)他們的預(yù)訂已過(guò)期。
- 向所有持有該場(chǎng)次等待用戶(hù)的WaitingUserService服務(wù)器廣播消息,以找出等待時(shí)間最長(zhǎng)的用戶(hù)。一致性哈希方案將告訴我們哪些服務(wù)器持有這些用戶(hù)。
- 如果所需的座位已經(jīng)變?yōu)榭捎茫拖虺钟凶铋L(zhǎng)等待用戶(hù)的WaitingUserService服務(wù)器發(fā)送消息以處理他們的請(qǐng)求。
每當(dāng)一個(gè)預(yù)訂成功時(shí),將發(fā)生以下事情:
- 持有該預(yù)訂的服務(wù)器向所有持有該場(chǎng)次等待用戶(hù)的服務(wù)器發(fā)送消息,以便這些服務(wù)器可以使所有需要的座位數(shù)多于可用座位數(shù)的等待用戶(hù)過(guò)期。
- 收到上述消息后,所有持有等待用戶(hù)的服務(wù)器將查詢(xún)數(shù)據(jù)庫(kù),以查找現(xiàn)在有多少個(gè)空閑座位。此處的數(shù)據(jù)庫(kù)緩存將大大有助于只運(yùn)行一次這個(gè)查詢(xún)。
- 使所有希望預(yù)訂的座位數(shù)多于可用座位數(shù)的等待用戶(hù)過(guò)期。為此,WaitingUserService必須遍歷所有等待用戶(hù)的Linked HashMap。