最近論壇里已經慢慢有人在考慮票池的設計了,這是我關於票池架構的一些想法。具體的討論請去論壇上討論:http://12306ng.org/thread-1572-1-1.html 需求討論 到目前為止,我了解到票池需求有: 1、車票的預售期不定,有30天的,也有10天的,但應該是10天的居多。 2、票在事先有計划售票管理,制定票額計划和編制臨時票額計划。這樣一來票是動態分配的,在預售期的前幾天,會根據熱門乘車區間預先分配一些票,在預售期后面幾天,會將沒賣完的回收到票池里。 3、需要考慮退票和改簽的問題。 4、需要考慮有一部分票可能是預留給一些特別的單位,這些預留票可能最后沒有賣出去,回收到票池中。 5、需要考慮中途上下車的情形,為了保證客運的產出,最好是同一個座位,盡可能地多賣票。比如說,上海到北京的火車,如果有乘客甲買了上海到南京,乙買了南京到北京的車票,從座位的使用效率來講,當然是甲和乙都坐一個位子就好了。 應該還有其他的需求,我建議一開始可以縮小需求范圍,避免需求膨脹,我覺得開始可以只考慮下面的需求: 1、只支持10天預售。 2、不支持計划,也就是我們從上游計划系統獲取已經計划好的票額,放進票池中。 3、支持退票和改簽。 4、支持預留票。 5、支持中途上下車的情形,一個座位重復賣票的問題。 架構設計思路 票池的架構應該考慮下面幾個問題: 1、票池應該可以方便分布式,從上面的需求來看,票池至少可以從兩個維度考慮分布,首先根據售票的地點,即車票的始發站分布。這樣一來,相應的票池服務器離乘客是最近的,對於異地購票的乘客,可以直接重定向到異地服務器,或者在本地緩存一些票都是可以的;再就是可以根據時間分布,即1天后開車的車票和9天后開車的車票是完全可以放在不同的服務器上的。 2、為了保證售票的速度,應該盡量將整個票池放在內存中,一些關鍵的數據盡量放在CPU緩存里。這是因為,硬盤的隨機訪問速度是內存訪問速度的10000倍,硬盤的順序訪問速度要比隨機訪問速度快很多;而CPU二級緩存的訪問速度又比內存快2到3倍,一級緩存要比內存快10倍左右。 3、CPU將數據讀取到緩存的過程一般是批量讀取的,而不是一個字節一個字節讀取;為了能夠盡可能地利用上CPU緩存,因此要盡量將相關數據放在連續的內存里。這樣一來,最好是盡量使用數組結構,而不是鏈表。 4、使用鏈表等非連續結構還有幾個問題,第一在分配內存時,對於C/C++這樣的程序,在分配內存時查找空閑內存比較耗時間,對於Java等基於垃圾回收語言,GC后更新鏈表的引用也是一個問題。第二就是內存碎片問題,對於長期在線的服務器,我覺得應該盡量避免使用鏈表結構。 5、盡可能的無鎖操作,即使整個票池都在內存里,如果是需要鎖來同步多線程的話,會有幾個問題,第一是需要從用戶態切換到內核態,這個過程可能需要執行幾千個甚至更多的指令;第二是因為線程來回切換,原先CPU緩存的代碼和數據都將無效,需要來回在緩存和內存倒騰數據。 6、在對票池並發處理時,我覺得應該只有一個線程負責寫入信息,其他的線程都只負責讀取。不使用多線程寫入的好處是,第一可以實現無鎖;第二可以避免偽共享問題。 現有方案對比 我在之前的帖子里提到了使用有向圖的設計方案,我現在依然堅持這個方案 - 不過改成用有向圖做索引,我先對比一下論壇上其他幾個方案(詳細情況參看:http://12306ng.org/forum.php?mod ... 01&fromuid=5805): 1、二進制的方案,雖然在我的設計里會有類似二進制的方案,但是原始二進制方案的一個很大的問題是,好像沒有考慮數據庫自身的實現,例如在帖子里說是編寫類似的查詢: where (station>0011111100) and (not (station&0011111100)^0011111100) limit 10 上面的條件子句,從數據庫實現的角度來說,需要考慮怎么建立索引,B樹應該是不能建立這樣支持按位操作的索引的(如果可以的話請糾正我),不過不知道位圖索引是否可以支持 – 但mysql好像不支持位圖索引。如果沒有一個很有效的索引解決方案的話,在數據庫中使用二進制方案恐怕會變成逐行掃描 - 也就是有大量的磁盤訪問,效率就很低了 。 2、兩個整數表示始發站和結束站,這個方案會經常維護二叉樹結構,而且樹的節點個數和高度都不是確定的 – 這是因為一個座位如果拆分成多個短途訂單,這個座位會在二叉樹里有多個節點。
在上圖里面,可以看到,每個站點(就是圖里面的節點)用一個列表保存了經過它的所有的車次(邊),通過有向邊的方式指明車次的方向,一個車次其實是由多條邊組成的。 可以把站點(例如北京)和車次(例如G017)本身看成獲取數據的索引,例如在server-core/cpp/sites.h里,將所有的站點定義成一個枚舉型;server-core/cpp/trains.h里,將所有的車次定義成一個枚舉型(以數字開頭的,在前面加上下划線就可以了)。由於站點和車次不是經常更換,因此可以固定起來,以后有更新的話,只需要提供站點和車次的配置文件,直接生成上面兩個代碼就可以了,如果買票訂單保存的是起始和終點站的索引的話,在重新生成的時候就需要考慮保證相同站點名的索引值不變,但如果訂單直接保存站點名稱,就沒必要保證索引值不變了。 索引如下圖的二維表所示,其中上面兩個數組分別是車次G108和G107的余票信息,“-”表示這個位置車次經過該站點,它的值實際是一個指針,指向對應車次的余票數組: ![]() 又因為需要考慮中間上車的情況,二進制的方案如果是放在數據庫里,會有很大的性能的問題,那么我在考慮是否可以將二進制的方案整個放在內存呢?我覺得是可能的,主要是出於下面幾個發現: 1. 首先在上圖里,車次的余票信息的確是一個大數組,這個數組可以是一個位數組,每一位代表這個座位的售票情況,只要這個座位有過售票 - 不管是從始發站坐到終點站的,還是中間上車的,那么就將這個位設成1。而一個車次的車廂配置、車廂的座位、鋪位配置在一個固定的時間段,至少是一天內是固定的,可以認為是不經常改變的。 2. 還沒有賣出去票,是不需要保存在內存里,只要在上面的數組里將對應位設為0就好了。 3. 所有從始發站坐到終點站的車票也不需要保留在內存里,只要在上面的數組里將對應位設為1就好了. 4. 在內存里我們只要找到一個數據結構,用來保存中間會上下車的座位信息就可以了,這個信息就可以用二進制的方案來表述,第一是占用的內存量小,第二是對比和修改都很快。 5. 至於退票,我還在考慮是放回票池,還是用一個單獨的鏈表結構來保存,我現在傾向於放回票池。 6. 那保存每個車次的中間上下車的余票信息,我們可以借鑒Windows系統管理內存分配的數據結構,這個結構可以做成一個包含數組的數組,數組的下標代表這個位置的元素的空閑位數,如下圖所示: 每個車次都有類似上圖的二維數組,在上圖里,數組的第一個元素里,包含的是該車次所有最大連續空閑站點數為1的座位,也就是說只有1站沒有人坐的位置;第二個元素,是該車次有連續2站沒有人坐的位置,雖然第二個元素我們看到實際是有三站空余,但我們仍然放在第二個元素里。 這個時候,如果有人買票,例如是坐一站的,那我們就首先去第一個數組里找,找到第一個匹配,將位補齊,這個時候發現位置已滿,因此將其從上圖的數組中移除,移除它剩下的空就放在那里,如下圖所示:
如果有人買兩站的票,跟上面一樣,找到第二個數組的第一個座位匹配,買了票之后,它的值變成:“11111101”,因為它只有一站是空閑的,因此我們將其放到第一個數組中去,如下圖所示:
這個二維數組,每一個車次的列數是固定的 – 因為每個車次經過的站點數目是固定的,而每列對應的數組,如果空間不夠了,可以動態分配(這里是一個風險,我還沒有仔細計算過極端情形)。 為了節省內存,每個座位的車次是一個長整形,即8個字節組成,這8個字節里,前14位用來表示座位在車次的索引(14位里,可以有12位表示索引,可以表示4096個座位,應該可以滿足一趟車上的坐票、卧鋪和站票信息了,另外兩位可以用來做一些標志位,具體干什么我還沒有想好),后50位就是座位的站點占用信息。如下圖所示: ![]() 對於運行區間超過50個站點的車次,作為特殊車次特殊處理 - 這樣的車次應該不是很多,可以先枚舉下。 還有對分布式的支持、負載均衡等方面的想法,還沒有寫完,這兩周慢慢寫,先把現在想到的發出來,拋磚引玉。 |