對於12306,我的完整技術方案

2021-09-01 01:55:42 字數 3229 閱讀 9296

12306主要就是賣票比較複雜,註冊登入之類的功能就不說了。

有網友說,12306賣票系統比航空複雜,因為要分段賣,航空只有起點和終點,火車中間還有好多站。不過好訊息是,這些站在售票時是連續的,不會出現1張票跳著站買的情況,這樣就可以把一張票拆成n張只有起點和終點的票,和航空售票一樣了。(我不了解航空售票,也不了解火車售票業務具體模型,下面都是基於推測和假設之類的。)

賣票分為兩部分,查詢和購買。12306目前提供了單獨的查詢,我覺得這個其實挺好的,至少有讀寫分離的思想;不過延遲10分鐘的資料,肯定沒什麼人願意用,大家還是要擠進購買系統查詢。單獨的查詢相當於擺設了,沒有發揮作用。要讓查詢系統有效,尤其是春運期間,延遲應該在30秒之內。

[b]查詢剩餘票數設計:

[/b]

查詢的特點是按照車次資訊或者時間查,車次和時間一般都不會變,因此在設計時,可以把頁面分成兩部分。一是匹配的車次列表資訊,如北京到上海有哪些車,這個結果可以用cdn快取。車次基本是固定的,快取設定為10分鐘應該就能滿足需要。不占用負載。在瀏覽器拿到這個頁面後,通過非同步請求,根據每趟車的編號二次查詢剩餘票數等實時資料,合併顯示。

車次查詢時,把車次資訊分庫儲存,並作冗餘儲存。好比這個庫提供所有北京->***的查詢,這個庫提供上海到***的查詢,這個庫提供廣州到xx的查詢,剩下的在乙個大庫中等。車次變化很小,庫可以分散存,而且可以冗餘儲存。用記憶體表也行,總資料量也不多,效能上沒什麼可說的。

對於實時票數,確實是比較困難的,網上很多方案都不對,沒有考慮中間站的問題。剩餘票數快取並不好做。我的想法是,提前分好票,然後用單獨的資料結構做快取。

例如,對於g113高鐵,共有8站:北京、德州、濟南、徐州、南京、常州、蘇州、上海。假設它有兩千張票,座位啊臥鋪啊啥的。在發票前,建立新錶20120113_g113,然後把2000張票提前插入到表中,每個票都有乙個本表內唯一的數字編號。表結構基本上就是:編號、起始站、終點站、座位型別、車廂、座位號、乘客姓名、乘客證件號碼、車票狀態等實際業務模型需要。初始化時,起點站就是北京(根據順序儲存為1),終點站就是上海(根據順序儲存為8),乘客資訊空著表示尚未繫結乘客,車票狀態置為「待**」。

這樣我們要查詢2023年1月13號g113 濟南->南京 的剩餘票數時,就查詢20120113_g113起始站編號大於等於3並且小於等於5,並且狀態為「待**」的記錄數就行了。

每張表2000條資料,對於非春運時節,效能完全足夠。對於春運時節,非繁忙路段,效能應該也足夠了。對於春運繁忙期,繼續看下面的。

[b]車票**基本流程:

[/b]

使用者選擇車票並要求購買,系統鎖定票並標記狀態為「鎖定中」,讓使用者付款等。完成後標記車票為「已經發售」,並且更新使用者資訊到車票的持有人資訊欄位中。此票不再**。

對於中間站購票,假設使用者購買了 濟南->南京 ,前面流程不變。但完成出票後,將車票起始和終點站改為「濟南」和「南京」,並且自動插入兩張新票可用。一張是「北京->濟南」,一張是「南京->上海」,通知佇列更新相關快取。相當於車票做了自動**。這樣我們設計時只需要把一張票設計為「只有起點和終點」就行了。

[b]更高效的剩餘票數查詢設計:

[/b]

資料庫的count操作並不快,因此對於繁忙季節的繁忙表,每次都count是鐵定不行的。我想到的乙個辦法是:把上面提到的預售車票表載入到記憶體中。我們用乙個64位long型別數字表示一張車票,每趟車的每種座位型別是乙個long陣列。

對於每個long數字,前面的32位用來順序儲存32個車站(假設一趟車最多有32個站,沒查過,不行可以放長點),每一位標記「車票是否包含此站」。如g113 濟南到南京的票(車站順序為第3到第5站),long數字的第2到第4位設定為1,其他前32位設定為0。後面的32位用來表示車票在車票**表中的唯一編號。這樣根據乙個long型別數字,我們就能表述一張票的發售資訊了。

比如2023年1月13號g113,有軟臥500張和硬臥1500張。那就需要兩個陣列。20120113_g113_軟臥 對應乙個長度500的long陣列;20120113_g113_硬臥 對應乙個長度1500的long陣列。儲存所有售票資訊。

有點類似bitset的感覺,對空間要求不高。我們可以做個系統把所有車票資訊按照這種結構載入到記憶體中。對於實時查票,如查詢2023年1月13號g113車次 濟南->南京 的餘票,就是遍歷兩個陣列,檢查位數為2和4的long數字有多少個,就直接獲得了軟臥和硬臥的剩餘票數。對於現代計算機,這點遍歷,時間是納秒級的。

當車票被**後,從long陣列中刪除票資訊,比如先置為-1表示已經無效。再用後台執行緒實際刪除(避免寫衝突,將刪除延遲到重建乙個陣列所消耗的多少納秒內剛好沒有寫請求的時間段中)。如果long陣列長度為0,那就是沒有車票了,直接返回0;使用者再怎麼刷,也不會干擾資料庫。

[b]更高效的剩餘票數查詢方案的擴充套件性和容錯性:

[/b]

本身車票是按照車次劃分的,同時也有時間維度,橫向擴充套件不存在任何問題。

long陣列可以根據資料庫票務資訊重新構造,而且成本不高(一條「select * from *** where 狀態=待發售」 sql語句)。在硬體故障,擴容機器,或是發現資料不一致時,重新構造陣列就行。而且可以從後台非同步做。擴充套件性和容錯性都不是問題。

[b]售票交易與鎖票:

[/b]

使用者查詢到有票後,填寫要購買的票數,提交。好比購買兩張硬臥,流程如下:

1. 在20120113_g113_硬臥long陣列中隨機獲取符合要求的順序的兩個long數字,並將其從陣列中刪除;這個速度很快,納秒級;

2. 根據long數字後32位,從資料庫中鎖票。如果全部鎖定成功,設定票為預定狀態,更新使用者預定資訊,鎖票時間等。然後記錄日誌等其他相關操作。如果鎖票失敗,說明在納秒級的時間內,票還是有衝突,購票失敗,直接返回報錯。鎖票就是資料庫行鎖,sql中的 select for update nowait。

3. 頁面提示錯誤,或者進入下一步交易流程,如網銀支付等。

在整個過程中,我們看到,使用者請求就算集中爆發,事務的衝突性也能降低到「隨機獲取的long陣列值剛好一樣 + 在cpu執行2000個for迴圈的可能百萬分之一秒內剛好同時提交」。我覺得衝突概率應該很低很低。一趟車2000個車票,1:100比例也就20萬人同時搶,20萬人同1秒提交cpu也算的過來。而實際上,怎麼可能一秒鐘有20萬人同時搶一趟車的票哪……

在整個過程中,大部分請求都被long陣列消耗完後,直接檢查long陣列長度為0,提示無票攔截。進入資料庫階段的,也就是比實際的票數多一點點的有效訂單而已。

[b]回票:

[/b]

中間站買票的,在預定成功後,車票自動**,**的票可以通過佇列排程實時的回到long陣列中,繼續服務。

預定後不買的,可以通過預定時間的超時檢查,後台做個執行緒,讓票回歸。

歡迎討論。

微博:[url]

我設計的12306

feed系統和火車票售賣系統是2個高訪問高併發情況下具體很大挑戰的系統。在低訪問,低併發的情況下feed系統會變的非常簡單,資料模型和業務功能都比較容易設計和實現,主要的挑戰就剩如何面對層出不窮的敏感詞和花樣百出的廣告語。相比之下,火車票售賣系統在低併發時也很有趣,假設我是12306的架構師,我會如...

從非技術角度再談12306解決方案

技術上再完美也無法解決購票難問題,但肯定可以解決黃牛問題。不過我不是技術牛人。我想到如下兩個非技術層面的解決方案。不過第二個方案可行性應該很好。1 電信運營商,公安部門和購票系統配合。首先是使用者使用的手機號實名,手機號開通時身份證必須是真實的。使用者的身份的真實性可以通過手機驗證的方式進行驗證。購...

對於新舊技術的爭論,我很low的想法

身處it領域不可避免的會面對新舊技術的迭代更新,自然也少不了新舊技術誰好誰壞的爭論。因為最近自己也遇到了新舊技術的 戰爭 所以順道說說自己的看法,當然,我的看法很low。效率 or 可靠性?我們經常會聽到這樣的言論,新技術可以提公升效率,包括我自己也這麼說。不論是新技術還是老技術不可否認的是都可以提...