抢了个票,还以为发现了12306的系统BUG
同事過年要早回家,失去單身手速好多年的他,只能依賴搶票軟件定時(shí)來搶。
結(jié)果,兩個(gè)搶票軟件居然都成功了,一下?lián)屃藘蓚€(gè)車次。。。真是幾家歡喜幾家愁!
同一乘車人,同一行程,同一時(shí)間段
12306在很久以前,對購票和乘車規(guī)則是有限制的,當(dāng)同一乘車人的兩張車票涉及的行程出現(xiàn)沖突時(shí),會拒絕購票請求。
仔細(xì)想來,購票業(yè)務(wù)的真實(shí)場景非常復(fù)雜,單是任何區(qū)間段購票這一個(gè)場景,就涉及到了好多的問題,要兼顧不同區(qū)間段的旅客數(shù)量,分配不同的席位庫存。還要考慮到鐵路的收入,長行程旅客優(yōu)先等等(這就是為什么有時(shí)候本站出發(fā)無票,但往前多買幾站就會有票的原因吧)。
那么,12036是怎么做到席位按區(qū)間售賣,而又怎么做到行程沖突校驗(yàn)的呢?而現(xiàn)在,被購買了兩張行程沖突的車票,又是哪里出了問題呢?
客票系統(tǒng)架構(gòu)猜想
猜想1:全境所有的車票以及電子車票,都由總部統(tǒng)一分配調(diào)控嗎?
如果能統(tǒng)一收口應(yīng)該是最理想的狀態(tài)。但是,我理解現(xiàn)有的系統(tǒng)架構(gòu)不是被統(tǒng)一收口的。只要是坐過火車的都應(yīng)該聽過“某某鐵路局溫馨提示,xxxx”。而且,對于每個(gè)客運(yùn)段,其運(yùn)載能力和承載的旅客情況都是不一樣的,所以,每個(gè)客運(yùn)局來管理自己負(fù)責(zé)客運(yùn)段的票,才更符合實(shí)際情況和運(yùn)營需要。
猜想2:那么,乘客是怎么通過統(tǒng)一的購票平臺來訂購不同局段的票的呢?
理論上,是由平臺向各個(gè)涉及的客運(yùn)段查詢余票以及下發(fā)訂票請求。由下游客運(yùn)局操作自己的數(shù)據(jù)庫。并同步數(shù)據(jù)到購票平臺。
那么,整體的系統(tǒng)架構(gòu)可以來大致描畫一下了:
這種席位庫存分別維護(hù)的架構(gòu)下,雖然對獨(dú)立運(yùn)營的實(shí)際需求是友好的,但是對系統(tǒng)交互提出了更高的要求。涉及到系統(tǒng)間信息同步,特別是涉及到跨系統(tǒng)的庫存扣減和信息同步時(shí),就要求客票系統(tǒng)要有合理的大并發(fā)下的最終一致性方案。
怎樣保障橫跨多客運(yùn)段席位庫的最終一致性呢?
分布式事務(wù)是這類場景的一個(gè)較成熟的解決方案,就訂票場景來看,XA性能較差,用事務(wù)消息,時(shí)效性上會打折扣,TCC則是一個(gè)相對折中且成熟的方案:
TCC過程中要解決的問題:
?
冪等,由事務(wù)發(fā)起者生成全局唯一事務(wù)ID,參與者根據(jù)事務(wù)ID冪等。
?
網(wǎng)絡(luò)中斷導(dǎo)致一階段丟包,二階段允許空回滾;即鎖定資源為空的回滾請求,返回回滾成功。
?
網(wǎng)絡(luò)擁堵導(dǎo)致請求錯(cuò)序,二階段需要防資源懸掛;即,已經(jīng)回滾過的事務(wù)ID,不允許鎖定資源。
我們用TCC解決了分布式下跨系統(tǒng)分配資源的最終一致性問題,那么,對于文章開頭提到的購票限制,又該怎么做呢?
訂票業(yè)務(wù)模型猜想
一次購票行為包含哪些屬性
一張車票就是一個(gè)訂單,那訂單的主要屬性包括:
購票時(shí)間、車次、席位號、乘車人、乘客類型、出發(fā)地、目的地、出發(fā)時(shí)間、到達(dá)時(shí)間、行程區(qū)間集合?。
其中,行程區(qū)間集合?中的行程區(qū)間,需要包含區(qū)間ID?和 經(jīng)過該區(qū)間的開始結(jié)束時(shí)間?兩個(gè)部分。
這樣,配合出發(fā)時(shí)間,就可以唯一的刻畫出該用戶的一次旅途的路徑細(xì)節(jié)。
聽起來比較復(fù)雜,好在我們的列車行駛區(qū)間和對應(yīng)的運(yùn)行時(shí)刻表基本都是固定的,很少調(diào)整,即使有也會提前公布。
因此,可以采用兩個(gè)?BitMap?,分別對各個(gè)列車的區(qū)間 和 區(qū)間時(shí)間進(jìn)行提前刻畫.
用戶購票區(qū)間存儲設(shè)計(jì)
如下圖所示,在列車的整個(gè)行程bitmap中,將用戶購買的區(qū)間置為1 ,這樣,只要getBit==1 的區(qū)間就是用戶的行程區(qū)間:
用戶行程時(shí)間區(qū)間存儲設(shè)計(jì)
如下圖所以,將列車的整個(gè)行駛時(shí)長分段,比如按5分鐘分段,將用戶購買的車次對應(yīng)的區(qū)間耗時(shí)段置為1 :
當(dāng)我們希望獲取用戶的行程真實(shí)時(shí)間段時(shí),只需要用出發(fā)時(shí)間加上對應(yīng)的占用時(shí)間段耗時(shí)時(shí)長,即可。
余票庫存存儲設(shè)計(jì)
和用戶的bitmap初始值為0相反,庫存管理使用的bitmap初始值位總的席位數(shù),當(dāng)有用戶訂票成功后,對席位數(shù)進(jìn)行扣減:
余票和席位綁定設(shè)計(jì)
上面的庫存管理方案,只能籠統(tǒng)的來統(tǒng)計(jì)當(dāng)前的可用余票庫存,但是,購票時(shí),每個(gè)訂單都需要和席位綁定才行:
每個(gè)區(qū)間,除了總的席位數(shù),還將席位編號的鏈表掛在當(dāng)前區(qū)間上。1-1-A?則表1車廂-1號-A座。
當(dāng)用戶購買了一個(gè)中間區(qū)間的席位后,將對應(yīng)區(qū)間下掛靠的席位摘除:
而其他區(qū)間的相同席位,也摘除,重新掛在本區(qū)間的最后。這樣,可以保證所有區(qū)間所掛席位集合,每次獲取的第一個(gè)席位都是同一個(gè),可以最大努力的保證最長行程可以被優(yōu)先分配。
我們每次訂區(qū)間票的時(shí)候應(yīng)該都有體會,從這站訂沒有票,但是靠近始發(fā)地多定幾站就有票了。
區(qū)間沖突冪等控制
回到文章開頭的問題,如果要求同一乘車人,在 同一時(shí)間段車次不沖突 、不同車次行程不沖突,從冪等的角度來看,就需要根據(jù)?乘車人、行程區(qū)間?兩個(gè)屬性做冪等控制。
那么冪等應(yīng)該怎么做呢?
我們一般的冪等策略包括前端攔截 + 服務(wù)端校驗(yàn),這里我們只考慮服務(wù)端的冪等方案。
方案一:依賴數(shù)據(jù)庫唯一索引冪等
這個(gè)場景下,如果要做唯一索引,則需要用UID + 行程區(qū)間 。
但是如果采用上述的行程區(qū)間的組織方式,唯一索引的方式則不太好用了。
方案二:鎖
不管是分布式鎖,或數(shù)據(jù)庫鎖,核心思路是一樣的:先加鎖,防止對共享資源同時(shí)操作,來避免并發(fā)引起的資源沖突。
數(shù)據(jù)庫鎖一般的模式:一鎖 、二判 、三更新,其實(shí)更適合于對已有記錄進(jìn)行業(yè)務(wù)更新。
分布式鎖則更靈活一些:當(dāng)我們創(chuàng)建訂單時(shí),要先以用戶為key , 查詢緩存中存儲的區(qū)間bitmap和時(shí)間bitmap。用當(dāng)前訂單的bitmap 來比對 緩存中的bitmap,如果存在區(qū)間沖突或者時(shí)間沖突,則不再執(zhí)行后續(xù)操作。
總結(jié)
作為客票業(yè)務(wù)的行外人,沒法拿到詳細(xì)準(zhǔn)確系統(tǒng)設(shè)計(jì)資料,但這不妨礙我們就既有現(xiàn)象,對遇到的問題做個(gè)最基本的分析判斷。
難道真的是系統(tǒng)bug嗎?不至于吧!于是細(xì)查了下新聞,原來是12306把行程沖突的限制去掉了。。。白瞎了我這么多的分析~
不過也不能算白瞎,習(xí)慣性的從專業(yè)的角度去看待日?,F(xiàn)象,是我們系統(tǒng)研發(fā)工作者應(yīng)該刻意培養(yǎng)和具備的素質(zhì)之一。
很多人不想多關(guān)注業(yè)務(wù)場景,覺得和技術(shù)關(guān)系不大,但是,我卻認(rèn)為:任何技術(shù)的落地和實(shí)現(xiàn),都需要依托于真實(shí)的業(yè)務(wù)場景,只有更深入的理解了業(yè)務(wù)邏輯,才能更好的構(gòu)建更加合理、穩(wěn)定、可持續(xù)的技術(shù)架構(gòu)。
有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號
好文章,我在看??
總結(jié)
以上是生活随笔為你收集整理的抢了个票,还以为发现了12306的系统BUG的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一不小心节约了 591 台机器!
- 下一篇: 7000 字,四年多 Java 的 BA