立刻春節了,又到了火車票的銷售旺季,一票難求的問題依舊存在嗎?git
還記得10年前春節前買火車票得在放票前1天搬個小板凳去排隊,對於熱門路線,排一個晚上都有可能買不到票。github
隨着互聯網的發展,幾年前建設了12306網上購票系統,能夠從電腦上買票,可是不要覺得在電腦上就能買到票。算法
我記得12306剛推出時,常常發生12306網站打不開,沒法付款的問題。數據庫
爲何呢?數組
緣由很簡單,春節期間網上購票的人可能達到幾億的級別,並且放票日期是同一天同一個時間點,也就是說同一時刻12306要接受幾億用戶的訪問。架構
處理能力和實際的訪問需求更不上,帶來的結果就是網站打不開,系統不穩定的現象。併發
後來12306想了分線路分時段開啓的辦法,想辦法把不一樣線路的用戶錯開時間來訪問12306的網站,可是這個方法起初的效果不明顯,並非全部用戶都知道的(就好像你臨時通知今天不上班,但仍是有用戶會來單位的),因此大多數用戶仍是集中在一個點去訪問12306的網站。異步
隨着硬件的發展,技術的演進,12306的系統愈來愈趨於成熟,穩定性和響應速度也愈來愈好。數據庫設計
聽說如今不少商家還開通了雲搶票業務,本質上是讓你不要衝擊12306系統了,把需求提早收集,在放票時,這些系統會進行排隊與合併購買,這種手段能夠減小12306的訪問併發。函數
搶火車票是頗有意思的一個課題,對IT人的智商以及IT系統的健壯性,尤爲是數據庫的功能和性能都是一種挑戰。
接下來咱們一塊兒來縷一縷有哪些難點,又有怎樣的解決手段。
鐵路售票系統最基本的功能包括
查詢餘票、餘票統計、購票、車次變化、退票、改簽、中轉乘車規劃 等。
每一個需求都有各自的特色,例如
1. 查詢餘票,用戶在購票前一般會查一下到達目的地有哪些餘票,它屬於一個高併發的操做,同時須要統計餘票張數,須要很強的CPU來支撐實時的查詢。
2. 購票,購票和查詢不同,購票是會改變庫存的,因此對數據庫來講是更新的操做。
並且購票極可能發生衝突,例如不少人要買同一趟車的票,那就出現衝突了,到底賣給誰呢?
須要考慮鎖衝突,儘可能的讓不一樣的人購買時可並行,或者能夠合併多人的購票請求,來減小數據庫的更新操做。
3. 中轉乘車,當用戶須要購買的起點和到達站無票時,須要計算中轉的搭乘方案。
好比從北京到上海,若是沒有直達車,是否是該轉車呢?轉哪趟,在哪裏轉就成了問題,簡單一點就是買票的人本身想。
高級一點的話,可讓12306給你推薦路線,這個涉及的是數據庫的路徑規劃功能。
咱們來逐一分析一下這些需求的特色。
1. 普通的餘票查詢需求
你若是要買從北京到上海的火車票,一般會查一下哪些車次還有餘票。
查詢的過濾條件可能不少,好比
1.1. 上車站、下車站、中轉站
1.2. 車次類型(高鐵、動車、直達、快速、普客、...)
1.3. 出發日期、時段
1.4. 到達日期、時段
1.5. 席別(硬座、硬臥、...站票)
1.6. 過濾掉沒有餘票的車次
展現給用時還要考慮到怎麼排序(是按始發時間排呢,仍是按票價,或者按餘票數量排?),怎麼分頁。
眼見不必定爲實
查詢餘票一般不是實時的、或者說不必定是準確的,有多是後臺異步統計的結果。
即便是實時統計的結果,在高併發的搶票期間,你看到的信息對你來講也許很快就會失效。
好比你看到某趟車還有100張票,極可能等你付款的時候,已經賣光了。
因此在高峯期,餘票信息的參考價值並不大,不要被迷惑了。
2. 查詢餘票的另外一個更高級的需求是路徑規劃, 自動適配(根據用戶輸入的中轉站點s)
這個功能之前可能沒有,可是總有一天會暴露出來,特別是車票很緊張的狀況下。
就好比從北京到上海,直達的沒有了,系統能夠幫你看看轉一趟車的,轉2趟車的,轉N趟車的。(固然,轉的越多越複雜)。
從中轉這個角度來說,實際上已經扯上路徑規劃的技術了。
怎麼中轉是時間最短的、價格最低的、中轉次數最少的等等。(裏面還涉及轉車的輸入要求(好比用戶要求在一線城市轉車,或者必需要轉高鐵))。
關於路徑規劃,能夠參考一下PostgreSQL pgrouting,已支持多種路徑規劃算法,支持算法的自定義擴展。
簡直是居家旅行,殺人滅口的必備良藥。
1. 大多數用戶是有選擇綜合症的,一般來講,用戶可能會查詢不少次,才選到合適日期的合適車次的票。
查詢量比較大,春節期間更甚。
2. 爲了展現餘票數量,須要統計,會耗費較多的CPU, IO資源。
3. 路徑規劃,幫用戶選擇最佳的轉車路線,很考驗數據庫的功能,大多數數據庫沒有這個功能。
對於售票系統來講,查詢餘票其實是一個統計操做。
統計操做相比簡單查詢,不但消耗更多的IO還消耗更多的CPU資源。
想像一下幾億人(其實不用這麼多,可能幾十萬就夠了)來查詢餘票,即便機器沒掛掉,也會把全部機器的資源跑滿,CPU產生的熱量,可能幾分鐘就能把雞蛋煮熟咯。
爲了減小實時查詢餘票的開銷,一般會分時進行統計,更新最新的統計信息。
用戶查詢餘票信息時,查到的是統計後的結果,前面我已經分析過了,餘票是不可信的,因此存在必定的延遲其實也是容許的。
這下不能煮雞蛋了,由於把幾億個統計請求,變成了1個統計請求,是否是一會兒世界就冷靜了呢?
咱們能夠看到12306主頁的餘票大盤數據
1. 餘票信息須要統計,查詢會耗費較多的CPU,IO。
因爲餘票是不可信的,因此存在必定的延遲其實也是容許的,優化手段是異步統計,用戶查詢統計後的結果。
購票相對於查詢餘票來講,從請求量來分析,比查詢請求更少,由於一般來講,用戶可能會查詢不少次,才選到合適日期的合適車次的票。
可是因爲購票是一次交易,每次交易都會產生寫操做,並且這種交易並非無限庫存的交易,由於庫存是有限的,因此設計的關鍵是下降粒度,減小鎖衝突,減小數據掃描量。
另外還須要考慮的因素包括
1. 同一趟車次的同一個座位,在不一樣的維度可能會被屢次售賣
1.1 時間維度,如發車日期
1.2 空間維度,不一樣的起始站點
2. 票價
票價通常和席別綁定,按區間計費。
另外一個需求是儘可能的將票賣出去,減小空洞座位。
打個比方,從北京到上海的車,中間通過(天津、徐州、南京、無錫、蘇州),若是天津到南京段有人買了,剩下的沒有被購買的段應該還能夠繼續被購買。
若是一趟從北京到上海的車,全部的票都被蘇州到上海的用戶買了,其餘的位置沒有賣出,鐵大哥是否是要哭暈在廁所。
又或者某趟車大量的座位被中途上車的用戶買了,是否是能夠買到全程的票數就少了。
之前就存在這種狀況,對鐵大哥的成本是個不小的考驗。
1. 爲了減小購票系統的寫鎖衝突,例如同一個座位,儘可能不出現由於一個會話在更新它,其餘會話須要等待的狀況。
(好比A用戶買了北京到天津的,B用戶買了天津到上海的同一趟車的同一個座位,那麼應該設計合理的合併操做(如數據庫內核改進)或者從設計上避免鎖等待)
其實就是把座位的空間維度(從哪裏到哪裏)、自己的屬性(座位號)、時間維度(發車日期)進行解耦,放到多條記錄中,從而在購買時,能夠同時進行。
由於數據庫中最小的鎖目前是行鎖(單行記錄同一時刻只容許一個會話進行更新,其餘的被堵塞,等待釋放鎖),也許隨着技術的發展,會演變成列鎖,或者列裏面的元素鎖(好比數組,JSON)。
春節來臨時、一般須要對某些熱門線路增長車次。
及車次的新增、刪除和變動需求。
在設計數據庫時,應該考慮到這一點。
車次的變動簡直是牽一髮而動全身,好比餘票統計會跟着變化,查詢系統也要跟着變化。
還有初始化信息的準備,例如爲了加快購票的速度,可能會將車次的數據提早準備好(也許是每一個座位一條記錄),參考第3個需求的解說。
票多是通過不少渠道賣出去的,例如支付寶、去哪兒、攜程、鐵老大的售票窗口、銀行的代理窗口、客運機構 等等。
涉及到實際的銷售信息與資金往來的對帳需求。
一般這個操做是隔天延遲對帳的。
退票和改簽也是比較常見的需求,特別是如今APP流行起來,退改簽都很方便。
這就致使了用戶可能會先買好一些,特別是春節期間,用戶沒法預先知道何時請假回家,因此先買幾張不一樣日期的,到時候提早退票或者改簽。
改簽和退票就涉及到位置回收(對數據庫來講也許是更新數據),改簽還涉及購票一樣的流程。
這個就很簡單了,就是按照用戶ID,查詢已購買,未打印的車票。
學生票、團體票、臥鋪、站票
這裏特別是站票,站票是有上限的,須要控制一趟車的站票人數
站票一樣有起點和終點,可是有些用戶可能買不到終點的票,會先買一段的,而後補票或者就一直在車上不下車,下車後再補票。
這個手段極其惡劣,不過不少人都是這麼幹的,未婚先孕,如今的年輕人啊。。。。
一般會考慮容積率,避免站票太多。
若是無節制的銷售站票,可能坐不下的。
1. 大多數用戶是有選擇綜合症的,一般來講,用戶可能會查詢不少次,才選到合適日期的合適車次的票。
查詢量比較大,春節期間更甚。
2. 爲了展現餘票數量,須要統計,會耗費較多的CPU, IO資源。
3. 路徑規劃的需求,幫用戶找出(時間最短、行程最短、指定中轉站、最廉價、或者站票最少)等條件的中轉搭乘路線。
媽媽不再用擔憂買不到票啦。
4. 餘票信息須要統計,查詢會耗費較多的CPU,IO。
因爲餘票是不可信的,因此存在必定的延遲其實也是容許的,優化手段是異步統計,用戶查詢統計後的結果。
5. 爲了減小購票系統的寫鎖衝突,例如同一個座位,儘可能不出現由於一個會話在更新它,其餘會話須要等待的狀況。
(好比A用戶買了北京到天津的,B用戶買了天津到上海的同一趟車的同一個座位,那麼應該設計合理的合併操做(如數據庫內核改進)或者從設計上避免鎖等待)
其實就是把座位的空間維度(從哪裏到哪裏)、自己的屬性(座位號)、時間維度(發車日期)進行解耦,放到多條記錄中,從而在購買時,能夠同時進行。
由於數據庫中最小的鎖目前是行鎖(單行記錄同一時刻只容許一個會話進行更新,其餘的被堵塞,等待釋放鎖),也許隨着技術的發展,會演變成列鎖,或者列裏面的元素鎖(好比數組,JSON)。
6. 車次的變動簡直是牽一髮而動全身,好比餘票統計會跟着變化,查詢系統也要跟着變化。
還有初始化信息的準備,例如爲了加快購票的速度,可能會將車次的數據提早準備好(也許是每一個座位一條記錄),參考第3個需求的解說。
綜合以上痛點和需求分析,咱們在設計時應儘可能避免鎖等待,避免實時餘票查詢,同時還要避免席位空洞。
通過前面的分析,已經把鐵路售票系統最關鍵的幾個業務場景進行了描述,而且闡述了其中的設計痛點,那麼咱們如何設計合理的系統來知足幾億人民搶票的需求呢?
西遊記裏每一集孫悟空師父被妖怪抓走,總能找到救兵來解救。
咱們也須要救兵,救兵快來啊。。。。
PostgreSQL是全世界最高級的開源數據庫,幾乎適用於任何場景。
有不少特性是能夠用來加快開發效率,知足架構需求的。
針對鐵路售票系統,能夠用到哪些救命法寶呢?
1. 看招,法寶1,varbit類型
使用varbit存儲每趟車的每一個座位途徑站點是否已銷售。
例如 G1921車次,從北京到上海,途徑天津、徐州、南京、蘇州。包括起始站,總共6個站點。 那麼使用6個比特位來表示。
'000000'
若是我要買從天津到徐州的,這個值變動爲(下車站的BIT不須要設置)
'010000'
這個位置還能夠賣從北京到天津,從徐州到終點的任意站點。
餘票統計也很方便,對整個車次根據BIT作聚合計算便可。
統計任意組合站點的餘票( 北京-天津, 北京-徐州, 北京-南京, 北京-蘇州, 北京-上海, 天津-徐州, 天津-南京, ......, 蘇州-上海 )
udf_count(varbit) returns record
統計指定起始站點的餘票(start: 北京, end: 南京; 則返回的是 北京-南京 的餘票)
udf_count(varbit, start, end) returns record
以上兩個需求,開發對應的聚合函數便可,其實就是一些指定範圍的bitand的count操做。
經過法寶1,解決了統計餘票的需求、售票無空洞的需求。
2. 看招,法寶2,數組類型
使用數組存儲每趟車的起始站點,途經站點。
使用數組來存儲,好處是可使用到數組的GIN索引,快速的檢索哪些車次是能夠搭乘的。
例如查詢從北京到南京的車次。
select 車次 from 全國列車時刻表 where column_arr @> array['北京','南京'];
這條SQL是能夠走索引的,效率很是高,每秒請求幾十萬不是問題。
法寶2解決了高併發請求查詢符合條件的列車信息的需求。
3. 看招,法寶3,skip locked
這個特性是跳過已被鎖定的行,好比用戶在購買某一趟從北京到南京的車票時,實際上是一次UPDATE ... SET BIT的操做。
可是極可能其餘用戶也在購買,可能就會出現鎖衝突,爲了不這個狀況發生,能夠skip locked,跳過鎖衝突,直接找另外一個座位。
select * from table where column1='車次號' -- 指定車次 and column2='車第二天期' -- 指定發車日期 -- and mod(pg_backend_pid(),100) = mod(pk,100) -- 提升併發,若是有多個鏈接併發的在更新,能夠直接分開落到不一樣的行,可是可能某些pID賣完了,可能會找不到票,建議不要開啓這個條件 and column4='席別' -- 指定席別 and getbit(column3, 開始站點位置, 結束站點位置-1) = '0...0' -- 獲取起始位置的BIT位,要求所有爲0 order by column3 desc -- 這個目的是先把已經賣了散票的的座位拿來賣,也符合鐵大哥的思想,儘可能把起點和重點的票賣出去,減小空洞 for update skip locked -- 跳過被鎖的行,老牛逼了,不須要鎖等待 limit ?; -- 要買幾張票
法寶3解決了一夥人來搶票時,在同一趟車的座位發生衝突的問題。
4. 看招,法寶4,cursor
若是要查詢大量記錄,可使用cursor,減小重複掃描。
5. 看招,法寶5,路徑規劃
若是用戶選擇直達車已經無票了,能夠自動計算如何轉乘,根據用戶的乘車站點和目的地選擇最佳搭乘路線。
參考一下pgrouting,與物流的動態路徑規劃需求一致。
6. 看招,法寶6,多核並行計算
開源也支持多核並行計算的,在生成餘票統計時,爲了提升生成速度,能夠將更多的CPU加入進來並行計算,快速獲得餘票統計。
就好比你策劃了一本書,已經列好了大綱,同時你找了100個做者,這100個做者能夠根據你分配的工做,同時開始寫做,很快就能把一本書寫完。
而傳統的狀況,一本書,只能一個做者幫你寫,即便你找了100個做者,另外的99位也只能空閒,或者他們只能寫其餘的99本書。
7. 看招,法寶7,資源隔離
PostgreSQL爲進程模型,因此能夠控制每一個進程的資源開銷,包括(CPU,IOPS,MEMORY,network),在鐵路售票系統中,查詢和售票是最關鍵的需求,使用這種方法,能夠在關鍵時刻保證關鍵業務有足夠的資源,流暢運行。
這個思想和雙十一護航也是同樣的,在雙十一期間,會關掉一些沒必要要的業務,保證主要業務的資源,以及它們的流暢運行。
8. 看招,法寶8,分庫分表
鐵路數據達到了海量數據的級別,很顯然一臺機器沒法存下全部的鐵路數據。
那麼怎麼辦呢? 能夠將鐵路的數據進行分區存儲,存到不一樣的主機。
PostgreSQL的分庫分表方案不少,例如plproxy, pgpool-II, pg-xl, pg-xc, citus等等.
9. 看招,法寶9,遞歸查詢
鐵路有很是典型的上下文相關特性,例如一趟車途徑N個站點,全國鐵路組成了一個很大的鐵路網。
遞歸查詢能夠根據某一個節點,向上或者向下遞歸搜索相關的站點。
好比在有哪些車能夠直達北京,有哪些車能夠轉車到達北京,又或者查詢從北京到拉薩,有哪些線路以及途經線路能夠走。
10. 看招,法寶10,MPP,打完收工
爲了持續的提升12306的體驗,鐵大哥還有數據挖掘的需求,好比今年春節應該對哪些線路增長車次,天天的車次增長的規劃,哪些線路能夠減小車次也能在春節前將用戶送回家。
這些問題能夠基於以往的運輸數據進行挖掘計算,進行回答。
基於PostgreSQL的MPP產品不少,例如Postgres-XL, Greenplum, Hawq, REDSHIFT, paraccl, 等等。
使用PG能夠和這些產品很好的融合,保持語法一致。
下降數據分析的開發成本。
10道法寶一出,師父又回來啦。
猴子請來的救兵厲害吧,別急,還有更厲害的,阿里雲在PostgreSQL基礎上作了不少的改進,好比對12306的系統,就有特別的定製特性。
在鐵路購票系統中,有幾個需求須要用到bit和array的特殊功能,這些特殊的功能目前社區版本沒有,阿里雲RDS PostgreSQL對此作了加強,以下。
1. 餘票統計
統計指定bit範圍=全0的計數
不指定範圍,查詢任意組合的bit範圍全=0的計數
2. 購票
指定bit位置過濾、取出、設置對應的bit值
根據數組值取其位置下標。
回顧一下我以前寫的兩篇文章,也是使用varbit的應用場景,有殊途同歸之妙
《基於 阿里雲 RDS PostgreSQL 打造實時用戶畫像推薦系統》
《門禁廣告銷售系統需求剖析 與 PostgreSQL數據庫實現》
PostgreSQL的bit, array功能已經很強大,阿里雲RDS PostgreSQL的bitpack也是用戶實際應用中的需求提煉的新功能,大夥一塊兒來給阿里雲提需求。
打造屬於國人的PostgreSQL。
本文從鐵路購票系統的需求出發,分析了購票系統的部分痛點,以及數據庫設計時須要注意的事項。
PostgreSQL的10個特性,以及阿里雲對PostgreSQL的改進,能夠很好的知足鐵路購票系統的需求。
1. 使用varbit存儲每趟車的每一個座位途徑站點是否已銷售。解決了統計餘票的需求、售票無空洞的需求。
2. 使用數組存儲每趟車的起始站點,途經站點。數組類型支持索引,解決了高併發請求查詢符合條件的列車信息的需求。
3. 使用skip locked特性,解決了一夥人來搶票時,在同一趟車的座位發生衝突的問題。
4. 使用pgrouting路徑規劃特性,解決了智能推薦乘車的需求。
同時還能夠用在不少場景,好比金融風險控制,刑偵,社會關係分析,人脈分析等。
若是你感興趣,網上有不少分析PostgreSQL, pgrouting, Neo4j的文章,PostgreSQL甚至比Neo4j更適合graph場景.
5. 多核並行計算,讓更多的CPU同時幫你幹活,例如快速的異步餘票統計。
6. 減小坐席空洞的產生,保證更多的人能夠購買到全程票。(購票時,若是是中途票,儘可能選擇已售的中途票)
7. 根據每一個進程進行資源隔離,能夠提升穩定性。
8. 對接HybridDB (基於GP\HAWQ) MPP系統,語法一致,能夠支持鐵路系統的數據挖掘需求,節約了開發成本。
阿里雲長期提供PostgreSQL, HybridDB ( 基於Greenplum, HAWQ ) 服務和支持。