你若是指的邏輯關系的話,可以這樣去設計,一列火車會途徑很多個站,那麼:
火車信息 1 <==> N途經車站表 (其中含有火車的id)
② 淺論12306網上購票系統的設計
12306可以說是一個成功型的案例。
你對他不好的印象是來自於12306剛出來那年,搶火車票導致各種崩潰,載入不出來,特別難辨認的驗證碼,還要安裝什麼安全的證書。
後來幾年12306做了重大的底層技術變革,但是網頁UI並沒有換(關於UI這個東西,更換UI對用戶來說是有學習代價的,尤其12306是一款面相全國人民的網頁和app,UI是不能隨便換的)
所以你就會理所當然的認為現在的12306還是以前的12306,非也,現在的12306每天能扛得住30億次查詢請求。
通過官方的網頁、鐵路12306 app、攜程、去哪兒、超級火車票,還有各種民間程序員寫的搶票工具等等,包括各種瀏覽器插件不停的刷新搶票。還有人嫌棄自己不是下鋪,也要刷刷刷,改成滿意的為止。
有一張火車票被退了,其實這張票只是一個區間,比如西安始發,到北京終點這張票被退了,這樣按道理來說中間任意經停站到任意經停站都可以買票,等等,會有很多意想不到的需求,包含各種奇葩的組合
綜上所述,其實12306的業務邏輯遠比淘寶這樣的電商要復雜很多
需求分析、系統設計不僅要考慮到全國各省的情況,還要考慮窗口買票,電話買票不受互聯網購票的影響。
畢竟在窗口查出一張票,在用戶決定買它之前,這張票不可能被互聯網購票的人給搶了。也就是說互聯網購票在票源稀缺的情況下,和窗口電話購票相比是沒有優勢的。
互聯網購票之所以會失去優勢,主要原因就是12306購票系統為了支持每天大量的查詢請求,把票數的緩存時間調整的極長,筆者目測可能5分鍾都不止。
簡單說就是現在西安去北京的票是0張,突然有人退了票,這時2個人,一個窗口訂票,一個互聯網訂票。
互聯網訂票的人刷新發現還是0張,當然他也是不停地刷新,但是得到的是舊的緩存信息
窗口訂票的人花了5分鍾時間排隊,等到他的時候,互聯網訂票的人刷新出來了一張余票,他點擊購買,選乘坐人,提交訂單的時候,窗口排隊的人也在售票員那裡查到了一張余票,這時候互聯網訂票者提交的訂單會失敗,窗口訂票的人會成功買到這張寶貴的票源。
然而這個故事還沒有結束。在窗口購票的那個人開心的拿著自己的票走了之後,互聯網購票的人回到票源查詢界面刷新發現還是有一張余票,他看到的還是舊的緩存,點擊票源會收到不是最新票源的提示,直到5分鍾之後,票源會再次變為0。
簡單的來說,12606就是這樣設計的,從此春運再多人搶票也不會導致網站直接崩潰,大不了就是余票為0嘍。
參考資料:http://network.51cto.com/art/201401/427406.htm
③ 火車站售票管理系統
火車站售票管理系統一般都是C/S架構實現的,以前常用VB,VC等實現,現在也版有.NET版本和JAVA版的,不權過還是微軟的一套東西更容易做。呵呵,給你找了份VB6.0的,參考一下吧。希望對你有幫助。
http://www.jsjpaper.org/vb/13083.html
④ 不就是一個訂票網站嗎 12306 的核心模型設計思路究竟復雜在哪裡
本文的重點不是在如何解決高並發的問題,而是希望從業務角度去分析,12306 的理想模型應該是怎麼樣的。網上目前談 12306 的文章貌似都是千篇一律的只談技術,不談業務分析和如何建模的。所以我想寫一下自己的設計和大家交流學習。
1、需求概述
12306 這個系統,核心要解決的問題是網上售票。涉及到 2 個角色使用該系統:用戶、鐵道部。用戶的核心訴求是查詢余票、購票;鐵道部的核心訴求是售票。購票和售票其實是一個場景,對用戶來說是購票,對鐵道部來說是售票。因此,我們要設計一個在線的網站系統,解決用戶的查詢余票、購票,以及鐵道部的售票這 3 個核心訴求。看起來,這 3 個場景都是圍繞火車票展開的。
查詢余票:用戶輸入出發地、目的地、出發日三個條件,查詢可能存在的車次,用戶可以看到每個車次經過的站點名稱,以及每種座位的余票數量。
購票:購票分為訂票和付款兩個階段,本文重點分析訂票的模型設計和實現思路。
其實還有很多其他的需求,比如給不同的車次設定銷售座位數配額,以及不同的區段設置不同的限額。但相比前面兩個需求來說,我覺得這個需求相對次要一些。
2、需求分析
確實,12306 也是一個電商系統,而且看起來商品就是票了。因為如果把一張票看成是一個商品,那購票就類似於購買商品,然後每張票都有庫存,商品也有庫存的概念。但是如果我們仔細想想,會發現 12306 要復雜很多,因為我們無法預先確定好所有的票,如果非要確定,那隻能通過窮舉法了。
我們以北京西到深圳北的 G71 車次高鐵為例(這里只考慮南下的方向,不考慮深圳北到北京西的,那是另外一個車次,叫 G72),它有 17 個站(北京西是 01號站,深圳北是 17號站),3 種座位(商務、一等、二等)。表面看起來,這不就是 3 個商品嗎?G71 商務座、G71 一等座、G71 二等座。大部分輕易噴 12306 的技術人員(包括某些中等規模公司的專家、CTO)就是在這里栽第一個跟頭的。實際上,G71 有 136*3=408 種商品(408 個 SKU),怎麼算來的?如下:
如果賣北京西始發的,有 16 種賣法(因為後面有 16 個站),北京西到:保定、石家莊、鄭州、武漢、長沙、廣州、虎門、深圳。。。。都是一個獨立的商品,同理,石家莊上車的,有 15 種下車的可能,以此類推,單以上下車的站來計算,有 136 種票:16+15+14....+2+1=136。每種票都有 3 種座位,一共是 408 個商品。
為了方便後面的討論,我們先明確一下票是什麼?
一張票的核心信息包括:出發時間、出發地、目的地、車次、座位號。持有票的人就擁有了一個憑證,該憑證表示持有它的人可以坐某個車次的某個座位號,從某地到某地。所以,一張票,對用戶來說是一個憑證,對鐵道部來說是一個承諾;那對系統來說是什麼呢?不知道。這就是我們要分析業務,領域建模的原因,我們再繼續思考吧。
明白了票的核心信息後,我們再看看 G71 這個車次的高鐵,可以賣多少張票?
討論前先說明一下,一輛火車的物理座位數(站票也可以看成是一種座位,因為站票也有數量配額)不等於可用的最大配合。所有的物理座位不可能都通過 12306 網站來銷售,而是只會銷售一部分,比如 40%。其餘的還是會通過線下的方式銷售。不僅如此,可能有些站點上車的人會比較多,有些比較少,所以我們還會給不同的區間配置不同的限額。
比如 D31 北京南至上海共有 765 張,北京南有 260 張,楊柳青有 80 張,泰安有 76 張。如果楊柳青的 80 張票售完就會顯示無票,就算其他站有票也會顯示無票的。每個車次肯定會有各種座位的配額和限額的配置的,這種配置我目前無法預料,但我已經把這些規則都封裝近車次聚合根里了,所有的配置策略都是基於座位類型、站點、區間配置的。關於票的配置抽象出來,我覺得主要有 3 種:
某個區段最多允許出多少張;
某個區段最少允許出多少張;
某個站點上車的最多多少張。
當用戶訂票時,把用戶指定的區段和這 3 種配置條件進行比較,3 個條件都滿足,則可以出票。不滿足,則認為無票了。下面舉個例子:
ABCDEFG,這是所有站點。座位總配額是 100,假設 B 站點上車,E 站下車的人比較少,那我們就可以設定 BE 這個區段最多隻能出 10 張票。所以,只要是用戶的訂票是在這個區段內的,就最多出 10 張。再比如,一列車次,總共 100 個座位配額,希望全程票最少滿足 80 張,那我們只要給 AG 這個區段設定最少 80 張。那任何訂票請求,如果是子區間的,就不能超過 100-80,即 20 張。這兩種條件必須同時滿足,才允許出票。
但是,不管如何做配額和限額,我們總是針對某個車次進行配置,這些配置只是車次內部售票時的一些額外的判斷條件(業務規則),不影響車次模型的核心地位和對外暴露的功能。所以,為了本文討論的清楚起見,我後續的討論都不涉及配額和限額的問題,而是認為任何區段都可以享受火車最大的物理座位數。
並且,為了討論問題方便,我們減少一些站點來討論。假設某個車次有 A,B,C,D 四個站點。那 001 這個人購買了 A,B 這個區間,系統會分配給 001 一個座位 x;但是因為 001 坐到 B 站點後會下車,所以相當於 x 這個座位又空出來了,也就是說,從 B 站點開始,系統又可以認為 x 這個座位是可用的。所以,我們得出結論:同一個座位,其實可以同時出售 AB,BC 這兩張票。通過這個簡單的分析,我們知道,一列火車雖然只有有限的座位數,比如 1000 個座位。但可以賣出的票遠遠不止 1000 個。
還是以 A,B,C,D 四個站點為例,假如火車總共有 1000 個座位,那 AB 可以賣 1000 張,BC 也可以賣 1000 張,同樣,CD 也可以賣 1000 張。也就是說,理論上最多可以賣出 3000 張票。但是如果換一種賣法,所有人都是買 ABCD 的票,也就是說所有的票都是經過所有站點的,那就是最多隻能賣出 1000 張票了。而實際的場景,一定是介於 1000 到 3000 之間。然後實際的 G71 這個車次,有 17 個站,那到底可以賣出多少個票,大家應該可以算了吧。理論上這 17 個站中的任意兩個站點之間所形成的線段,都可以出售為一張票。我數學不好,算不太清楚,麻煩有數學好的人幫我算算,呵呵。
通過上面的分析,我們知道一張票的本質是某個車次的某一段區間(一條線段),這個區間包含了若干個站點。然後我們還發現,只要區間不重疊,那座位就不會發生競爭,可以被回收利用,也就是說,可以同時預先出售。
另外,經過更深入的分析,我們還發現區間有 4 種關系:
不重疊;
部分重疊;
完全重疊;
覆蓋。
不重疊的情況我們已經討論過了,而覆蓋也是重疊的一種。所以我們發現如果重疊,比如有兩個區間發生重疊,那重疊部分的區間(可能誇一個或多個站點)是在爭搶座位的。因為假設一列火車有 100 個座位,那每個原子區間(兩個相鄰站點的連線),最多允許重疊 99 次。
所以,經過上面的分析,我們知道了一個車次能夠出售一張車票的核心業務規則是什麼?就是:這張車票所包含的每個原子區間的重疊次數加 1 都不能超過車次的總座位數,實際上重疊次數 +1 也可以理解為線段的厚度。
3、模型設計
上面我分析了一下票的本質是什麼。那接下來我們再來看看怎麼設計模型,來快速實現購票的需求,重點是怎麼設計商品聚合以及減庫存的邏輯。
傳統電商的思路
如果按照普通電商的思路,把票(站點區間)設計為商品(聚合根),然後為票設計庫存數量。我個人覺得是很糟糕的。因為一方面這種聚合根非常多(上面的 G71 就有 408 個);另一方面,即便枚舉出來了,一次購票也一定會影響非常多其他聚合根的庫存數量(只要被部分或全部重疊的區間都受影響)。這樣的一次訂單處理的復雜度是難以評估的。而且這么多聚合根的更新要在一個事務里,這不是為難資料庫嗎?而且,這種設計必然帶來大量的事務的並發沖突,很可能導致資料庫死鎖。
總之,我認為這種是典型的由於領域模型的設計錯誤,導致並發沖突高、數據持久化落地困難。或者如果要解決並發問題,只能排隊單線程處理,但是仍然解決不了要在一個事務里修改大量聚合根的尷尬局面。
聽說 12306 是採用了 Pivotal Gemfire 這種高大上的內存資料庫,我對這個不太了解。我不可想像要是不使用內存資料庫,他們要怎麼實現車次內的票之間的數據強一致性(就是保證所有出售的票都是符合上面討論的業務規則的)?所以,這種設計,我個人認為是思維定勢了,把火車票看成是普通電商的商品來看待。所以,我們有時做設計又要依賴於經驗,又要不能被以往經驗所束縛,真的不容易,關鍵還是要根據具體的業務場景多多深入分析,盡量分析抽象出問題的本質出來,這樣才能對症下葯。那是否有其他的設計思路呢?
我的思路
1、聚合設計
通過上面的分析我們知道,其實任何一次購票都是針對某個車次的,我認為車次是負責處理訂票的聚合根。我們看看一個車次包含了哪些信息?一個車次包括了:
車次名稱,如 G71;
座位數,實際座位數會分類型,比如商務座 20 個,一等座 200 個;二等座 500 個;我們這里為了簡化問題,可以暫時忽略類型,我認為這個類型不影響核心的模型的設計決策。需要格外注意的是:這里的座位數不要理解為真實的物理座位數,很有可能比真實的座位數要少。因為我們不可能把一個車次的所有座位都在網上通過 12306 來出售,而是只出售一部分,具體出售多少,要由工作人員人工指定。
經過的站點信息(包括站點的 ID、站點名稱等),注意:車次還會記錄這些站點之間的順序關系;
出發時間;看過 GRASP 九大模式中的信息專家模式的同學應該知道,將職責分配給擁有執行該職責所需信息的類。
我們這個場景,車次具有一次出票的所有信息,所以我們應該把出票的職責交給車次。另外學過 DDD 的同學應該知道,聚合設計有一個原則,就是:聚合內強一致性,聚合之間最終一致性。經過上面的分析,我們知道要產生一張票,其實要影響很多和這個票對應的線段相交的其他票的可用數量。因為所有的站點信息都在車次聚合內部,所以車次聚合內部自然可以維護所有的原子區間,以及每個原子區間的可用票數(相當於是庫存數)。當一個原子區間的可用票數為 0 的時候,意味著火車針對這個區間的票已經賣完了。所以,我們完全可以讓車次這個聚合根來保證出票時對所有原子區間的可用票數的更新的強一致性。對於車次聚合根來說,這很簡單,因為只是幾次簡單的內存操作而已,耗時可以忽略。一列火車假如有 ABCD 四個站點,那原子區間就是 3 個。對於 G71,則是 16 個。
2、怎麼判斷是否能出票?
基於上面的聚合設計,出票時扣減庫存的邏輯是:
根據訂單信息,拿到出發地和目的地,然後獲取這段區間里的所有的原子區間。然後嘗試將每個原子區間的可用票數減 1,如果所有的原子區間都夠減,則購票成功;否則購票失敗,提示用戶該票已經賣完了。是不是很簡單呢?知道了出票的邏輯,那退票的邏輯也就很簡單了,就是把這個票的所有原子區間的可用票數加 1 就 OK 了。如果我們從線段的厚度的角度去考慮,那出票時,每個原子區間的厚度就是 +1,退票時就是減一。就是相反的操作,但本質是一樣的。
所以,通過這樣的思路,我們將一次訂票的處理控制在了一個聚合根里,用聚合根內的強一致性的特性保證了訂票處理的強一致性,同時也保證了性能,免去了並發沖突的可能性。傳統電商那種把票單做類似商品的核心聚合根的設計,我當時第一眼看到就覺得不妥。因為這違背了 DDD 強調的強一致性應該由聚合根來保證、聚合根之間的最終一致性通過 Saga 來保證的原則。
還有一個很重要的概念我想說一下我的看法,就是座位和區間的關系。因為有些朋友和我講,考慮座位號的問題,雖然都能減 1,座位號也必須是同一個。我覺得座位是全局共享的,和區段無關(也許我的理解完全有誤,請大家指正)。座位是一個物理概念,一個用戶成功購買了一張票後,座位就會少一個,一張票唯一對應一個座位,但是一個座位有可能會對應多張票;而區間是一個邏輯上的概念,區間的作用有兩個:1)表示票的出發地和目的地;2)記錄票的可用數額。如果區間能連通(即該區間內的每個原子區間的可用數額都大於 0),則表示允許擁有一個座位。所以,我覺得座位和票(區間)是兩個維度的概念。
3、如何為票分配座位?
我覺得車次聚合根內部應該維護所有該車次已經售出的票,已經出售的票的的本質是區間和座位的對應關系。系統處理訂票時,用戶提交過來的是一段區間。所以,系統應該做兩個事情:
先根據區間去判斷是否有可用的座位;
如果有可用座位,則再通過演算法去選擇一個可用的座位;
當得到一個可用座位後,就可以生成一張票了,然後保存這個票到車次聚合根內部即可。下面舉個例子:
假設現在的情況是座位有 3 個,站點有 4 個:
座位:1,2,3
站點:abcd
票的賣法 1:
票 1:ab,1
票 2:bc,2
票 3:cd,3
票 4:ac,3
票 5:bd,1
這種選座位的方式應該比較高效,因為總是優先從座位池裡去拿座位,只有在萬不得已的時候才會去回收可重復利用的票。
上面的 4,5 兩個票,就是考慮回收利用的結果。
票的賣法 2:
票 1:ab,1
票 2:bc,1
票 3:cd,1
票 4:ac,2
票 5:bd,3
這種選座位的方式應該相對低效,因為總是優先會去掃描是否有可回收的座位,而掃描相對直接從座位池裡去拿票總是成本相對要高的。
上面的 2,3 兩個票,就是考慮回收利用的結果。
但是,優先從座位池裡拿票的演算法有缺陷,就是會出現雖然第一步判斷認為有可用的座位,但是這個座位可能不是全程都是同一個座位。舉例:
假設現在的情況是座位有 3 個,站點有 4 個:
座位:1,2,3
站點:abcd
票的賣法 3:
票 1:ab,1
票 2:bc,2
票 3:cd,3
現在如果有人要買 ad 的票,那可用的座位有 2,或者 3。但是無論是 2 還是 3,都要這個乘客中途換車位。比如賣給他座位 2,那他 ab 是坐的座位 2,但是 bc 的時候要坐座位 1 的。否則拿票 2 的那個人上車時,發現座位 2 已經有人了。而通過優先回收利用的演算法,是沒這個問題的。
所以,從上面的分析我們也知道選座位的演算法該怎麼寫了,就是採用優先回收利用座位的演算法。我認為不管我們這里怎麼設計演算法,都不影響大局,因為這一切都只發生在車次聚合根內部,這就是預先設計好聚合根,明確出票職責在哪個對象上的好處。
4、模型分析總結
我認為票不是核心聚合根,票只是一次出票的結果,一個憑證而已。
12306 真正的核心聚合根應該是車次,車次具有出票的職責,一次出票具體做的事情有:
判斷是否可出票;
選擇可用的座位;
更新一次出票時所有原子區間的可用票數,用於判斷下次是否能出票;
維護所有已售出的票,用於為選擇可用座位提供依據。
通過這樣的模型設計,我們可以確保一次出票處理只會在一個車次聚合根內進行。這樣的好處是:
不需要依賴資料庫事務就能實現數據修改的強一致性,因為所有修改只在一個聚合根內發生;
在保證數據強一致性的同時還能提供很高的並發處理能力,具體設計見下面的架構設計。
4、架構設計
我覺得 12306 這樣的業務場景,非常適合使用 CQRS 架構;因為首先它是一個查多寫少、但是寫的業務邏輯非常復雜的系統。所以,非常適合做架構層面的讀寫分離,即採用 CQRS 架構。而且應該使用數據存儲也分離的 CQRS。這樣 CQ 兩端才可以完全不需要顧及對方的問題,各自優化自己的問題即可。我們可以在 C 端使用 DDD 領域模型的思路,用良好設計的領域模型實現復雜的業務規則和業務邏輯。而 Q 端則使用分布式緩存方案,實現可伸縮的查詢能力。
1、訂票的實現思路
同時藉助像 ENode 這樣的框架,我們可以實現 in-memory + Event Sourcing 的架構。Event Sourcing 技術,可以讓領域模型的所有狀態修改的持久化統一起來,本來要用 ORM 的方式保存聚合根最新狀態的,現在只需要簡單的通用的方式保存一個事件即可(一次訂票只涉及一個車次聚合根的修改,修改只產生一個事件,只需要持久化一個事件(一個 JSON 串)即可,保證了高性能,無須依賴事務,而且通過 ENode 可以解決並發問題)。
我們只要保存了聚合根每次變化的事件(事件的結構怎麼設計,本文不做多的介紹了,大家可以思考下),就相當於保存了聚合根的最新狀態。而正是由於 Event Sourcing 技術的引入,讓我們的模型可以一直存活在內存中,即可以使用 in-memory 技術。不要小看 in-memory 技術,in-memory 技術在某些方面對提高命令的處理性能非常有幫助。
比如就以我們車次聚合根處理出票的邏輯,假設某個車次有大量的命令發送到分布式消息隊列,然後有一台機器訂閱了這個隊列的消息,然後這台機器處理這個車次的訂票命令時,由於這個車次聚合根一直在內存,所以就省去了每次要去資料庫取出聚合根的步驟,相當於少了一次資料庫 IO。
這樣的好處是,因為一個車次能夠真正出售的票是有限的,因為座位就那麼幾個,比如就 1000 個座位,估計一般正常情況也就出個 2000 個左右的票吧(具體能出多少張票要取決於區間的相交程度,上面分析過)。也就是說,這個聚合根只會產生 2000 個事件,也就是說只會有 2000 個訂票命令的處理是會產生事件,並持久化事件;而其餘的大量命令,因為車次在內存計算後發現沒有餘票了,就不會做任何修改,也不會產生領域事件,這樣就可以直接處理下一個訂票命令了。這樣就可以大大提高處理訂票命令的性能。
另外一個問題我覺得還需要提一下,因為用戶訂票成功後,還需要付款。但用戶有可能不去付款或者沒有在規定的時間內完成付款。那這種情況下,系統會自動釋放該用戶之前訂購的票。所以基於這樣的需求,我們在業務上需要支持業務級別的 2pc。即先預扣庫存,也就是先佔住這張票一定時間(比如 15 分鍾),然後付款成功後再真實給你這張票,系統做真正的庫存修改。
通過這樣的預扣處理,可以保證不會出現超賣的情況。這個思路其實和傳統電商比如淘寶這樣的系統類似,我就不多展開了,我之前寫的 Conference 案例也是這樣的思路,大家有興趣的可以去看一下我之前錄制的視頻。
2、查詢余票的實現思路
我覺得余票的查詢的實現相對簡單。雖然對於 12306 來說,查詢的請求佔了 80%,提交訂單的請求只佔 20%。但查詢由於對數據沒有修改,所以我們完全可以使用分布式緩存來實現。我們只需要精心設計好緩存的 key 即可;緩存 key 的多少要看成本,如果所有可能的查詢都設計對應的 key,那時間復雜度為 1,查詢性能自然高;但代價也大,因為 key 多了。如果想 key 少一點,那查詢的復雜度自然要上去一點。所以緩存設計無非就是空間換時間的思路。然後,緩存的更新無非就是:自動失效、定時更新、主動通知 3 種。通過 CQRS 架構,由於 CQ 兩端是事件驅動的,當 C 端有任何狀態變化,都會產生對應的事件去通知 Q 端,所以我們幾乎可以做到 Q 端的准實時更新。
同時由於 CQ 兩端的完全解耦,Q 端我們可以設計多種存儲,如資料庫和緩存(Redis 等);資料庫用於線下維護關系型數據,緩存用戶實時查詢。資料庫和緩存的更新速度相互不受影響,因為是並行的。對同一個事件,可以 10 台機器負責更新緩存,100 台機器負責更新資料庫。即便資料庫的更新很慢,也不會影響緩存的更新進度。這就是 CQRS 架構的好處,CQ 的架構完全不同,且我們隨時可以重建一種新的 Q 端存儲。不知道大家體會到了沒有?
關於緩存 key 的設計,我覺得主要從查詢余票時傳遞的信息來考慮。12306 的關鍵查詢是:出發地、目的地、出發日期三個信息。我覺得有兩種 key 的設計思路:
直接設計了該查詢條件的 key,然後快速拿到車次信息,直接返回;這種方式就是要求我們系統已經枚舉了所有車次的所有可能出現的票(區間)的緩存 key,相信你一定知道這樣的 key 是非常多的。
不是枚舉所有區間,而是把每個車次的每個原子區間(相鄰的兩個站點所連成的直線)的可用票數作為 key。這樣,key 就非常少了,因為車次假如有 10000 個,然後每個車次平均 15 個區間,那也就 15W 個 key 而已。當我們要查詢時,只需要把用戶輸入的出發地和目的地之間的所有原子區間的可用票數都查出來,然後比較出最小可用票數的那個原子區間。則這個原子區間的可用票數就是用戶輸入的區間的可用票數了。當然,到這里我提到考慮出發日期。我認為出發日期是用來決定具體是哪個車次聚合根的。同一個車次,不同的日期,對應的聚合根實例是不同的,即便是同一天,也可能有多個車次聚合根,因為有些車次一天有幾班的,比如上午 9 點發車的一班,下午 3 點發車的一般。所以,我們也只要把日期也作為緩存 key 的一部分即可。
總結
本文完全是憑自己對 12306 這個網站的核心業務的簡單思考而得到的一些設計結果。如果真正的 DDD 領域建模,更多的是要和業務一線的工作人員、領域專家進行深入溝通,才能更深入的了解該領域內的業務知識,從而才能設計出更靠譜的領域模型和架構設計。
非常慚愧,我沒有上 12306 買過火車票,家離的比較近,就算要買也是家人給我買:)所以,本文所分享的內容難免是紙上談兵。但我覺得 12306 這個系統的業務確實比傳統的電商系統要復雜,且並發又這么高。所以,我覺得這個系統真的很值得大家重視模型的設計,而不只是只關注技術層面的實現。
⑤ 目前國家火車售票系統是B/S結構還是C/S結構
國家火車售票系統是B/S結構,QQ是典型的C/S結構。
⑥ 12306這個問題具體怎麼做
曾嗤之以鼻 現在認為幾乎是奇跡
1月11日起,12306網站開始銷售除夕當日火車票。每到此時,鐵路系統唯一的官方購票網站12306就會成為眾矢之的。今年也不例外,12306再次被淹沒在一片埋怨聲中。
1月5日,觀察者網刊登了問答網站「知乎」上的用戶王強的解答,回答「如果把12306外包給IBM或者阿里巴巴來做的話,能不能比現在做得好?」這一問題。
1月10日,一位ID名為「代碼狗」的前淘寶工程師,後來在一家電商公司做技術副總的IT業內人士也在著名論壇「西西河」上發文,表達了他自己對12306系統的看法。
值得注意的是,「代碼狗」在12306系統剛上線時也有過不少微詞。為了證明12306系統很容易搭建,「代碼狗」甚至曾經發起過一個名為「替12306設計系統」的開源項目。通過工作中的實踐,「代碼狗」對於12306系統也有了新的認識。
觀察者網轉載此文,供讀者參考。
全文如下:
官方訂票網站12306崩潰時的頁面(資料圖)
本人淘寶技術專家,2012年在一家百強民企做電商副總,當時在極為艱苦的條件下帶隊開發了一個B2C(企業針對個人開展的電子商務活動——觀察者網注)網站,走支付寶和銀聯支付通道,年營業額千萬級(作者註:當然實在太少了,我只是說這個網站投入了實際的運營)。
也就在那個時候,我對12306嗤之以鼻,覺得他們做得太爛了,認為自己能帶隊花幾百萬半年時間做個好的出來。於是我狂妄地想做一個開源的訂票系統給他們。我花了一個星期時間思考建立數據模型,思考到庫存這一步的時候,我才發現,12306的庫存復雜性比淘寶、京東高很多倍,運算量也大很多倍。傳統的分布式資料庫、緩存、負載均衡技術並不能恰好滿足12306的需求。
在平時,12306也就是個正常的電商網站。但一到黃金周,12306就是一個全站所有商品都秒殺,所有SKU都是動態庫存的變態。
即使不考慮線下既有的電話、代售點等渠道,要實現一個12306,最少最少也是千萬級別的硬體投入(作者註:這是當時的估算,沒有精算,可能與實際相差較大,總之,我說得不一定對,12306的業務也許沒我說的那麼復雜,但也絕不是某些人噴的那麼簡單),軟體和人力另算。那些叫囂只要40台伺服器、只要2個架構師4個程序員、大談分庫分表和前端CDN的人們,只是紙上談兵罷了。所謂初生牛犢不怕虎,做了三年CMS和BBS,就以這個經驗來噴12306,未免太天真了。
媒體人噴12306,是他們不懂技術,沒有能力和耐心來分析背後的難度。技術人員噴,則是因為大部分的技術人員在短時間思考時,容易陷入過於樂觀的誤區,經典的例子就是估算工作量,程序員們往往容易估算出一個超短的工期,把寫程序的工作樂觀地想像成了打字員照稿敲鍵盤的工作。
知乎那篇文章,我覺得不是洗地。排名第一和第二的答案都說得很客觀。淘寶技術是比12306強大很多倍,淘寶現在的系統也是花了10倍於12306的錢、時間和人才做起來的。根本原因還是鐵路運力不能滿足春運需求,淘寶也解決不了這個問題。
12306這一年來進步非常大。從前段動畫驗證碼、分時段搶票,到後端去小型機、虛擬化、內存資料庫的運用。可以說,12306是中國政府機關做的最強大的網站(電商系統),能在短短一兩年內做出這樣的改變,幾乎是個奇跡,就連一些市場化的民企都望塵莫及,甚至一些上市公司都比不上它!(比如51job和ctrip)。
事非經過不知難,在網上批判12306的人,大部分還是形成了【國企=壟斷+腐敗+低效】的思維定勢。小部分是真的輕視了它的難度。
至於12306一期工程3個億(含硬體)貴不貴我不評價,我只提供一個數字供參考,網路一年的研發費用(不含硬體)是10億,這個數字來自網路財報。網上能查到。3億看起來好大一個數字,真用到超大型的電商系統、搜索引擎系統裡面,其實也不算什麼天文數字了。
再解釋一下,為什麼秒殺壓力大,以及為什麼12306的動態庫存很復雜。
先說秒殺。
2013年12月25日前後,天貓搞了一個聖誕季積分兌換活動,持續幾天。25號上午10點12分,放出了15000個天貓魔盒(淘寶集市有人賣,大概190-230塊),從成交記錄上看,是19秒內全部搶完。
實際上,我也參加秒殺了,那天的題目特別簡單(請輸入xxx漢字的拼音首字母),我應該是5秒內答題完成並提交訂單,結果告訴我排隊的人太多,擠不進去,並提示14秒以後重試。人太多就是因為題目太簡單了,門檻越低,5秒內擠進去的人也越多嘛,如果題目換成【2克濃度為3%的U235在大亞灣核電站能發多少KW的電】,5分鍾之內也不會有1萬5千人跟我競爭。
我想,14秒以後哪還有我的事情呀,於是重新答題秒殺,結果出現了伺服器錯誤的頁面。反復刷新幾次,就告訴秒殺結束了。
在群里問了一下同事,有不到10個人回答我,都說沒秒到(也可能秒到的人悶聲發大財,不回復我)。
淘寶是什麼技術水平呢,淘寶有至少4000技術人員,至少4萬台伺服器(這都是兩年前的公開數據了,按規定可以談論),2013年11月11日成交額351億,2012年全年成交額超過1萬億。
淘寶擁有各種自主研發團隊:伺服器、交換機(網上可以搜索到淘寶公開的綠色伺服器開放標准);操作系統(LinuxKerneltaobao版,yunos手機操作系統是阿里雲的,暫時不計入)、Web伺服器(Tengine)、Java語言虛擬機(JVMtaobao版)、資料庫(MySQL內核taobao版,google和facebook也有自己的版本,HBase淘寶版、還有自己全部從頭開發的OceanBase)、負載均衡器(LVS,LVS始創人就在淘寶,擔任研究員)、Java運行容器(Jboss,其創始人之一,王文彬,也在淘寶,擔任副總裁)。
淘寶還有數不清的開源項目和中間件,如高性能Java通信中間件HSF、分布式資料庫中間件TDDL、非同步消息系統notify等等等等。
以淘寶這樣的技術水平,也不能做到秒殺時讓每個用戶都沒有擁擠感,為什麼呢?
一是要尊重物理原理,一台伺服器一秒鍾能承受的計算量是有極限的,任你怎麼優化,採用多高效的演算法和編程語言,都突破不了某個極限,比方說汽車發動機驅動的F1賽車至今也不能突破400公里的時速(超音速推進號那個1千多公里的時速不能算,那是飛機引擎驅動的)。再往深了說,就不容易懂了。感興趣的可以從著名的C10K問題開始看起。
二是要考慮經濟效益,十一黃金周的時候,北京主城區到八達嶺長城的路堵得嚴嚴實實,但不能因為黃金周的高峰,就把這段路修成長安街那樣10車道的高速公路。否則的話,花費天文數字(真的是天文數字,12306那3個億大概只夠修1-3公里)。修了一段路,黃金周是可以飆到80公里/小時了,可平時呢,拿來給兩邊的居民曬穀子?
淘寶目前的硬體和帶寬數量,已經超出日常運營的需求了,就是留了相當大的餘量給大促銷(眾所周知的是雙十一,雙十二,其實基本每個季度都有大促銷,每個月都有促銷,甚至天天都在促銷——聚劃算)。amazon當年就是為了應對黑色星期五的大促銷購置了大量的伺服器,平時訂單量沒那麼大了,amazon就把富餘的伺服器拿來搞雲計算了。順便說一下,阿里雲是當今中國第一世界數一數二的雲計算服務商,和amazon走的路也有點像。
再說動態庫存。
淘寶秒殺天貓魔盒的時候,只有一個商品(行話叫做SKU),它的庫存是15000個。有一個人秒殺到了,庫存就減1,19秒賣完的,一秒要成功產生789個訂單(下訂單的請求可能是8萬個,只是可能啊,非實際數字,也可能是1萬個,用於說明一下壯觀程度)。想像一下,你在廣場上賣火車票,一秒鍾有8萬人舉著錢對你喊:賣給我!
上過大學的人都知道,比秒小的時間單位還有毫秒、皮秒、飛秒。但交易系統登記一個交易可不像原子繞著原子核跑一圈那麼簡單,它要做這些事:檢查是否惡意訪問、取到系統時間、取到顧客默認收貨地址、核對顧客秒殺資格(當時的規定是天貓T2.T3達人)、生成訂單號、把顧客ID系統時間訂單號收貨地址寫入訂單系統、扣除顧客天貓積分、商品庫存減一、給顧客打標記(每人只能秒一個,下次不能秒了)等等,這每一件事都要花費毫秒級別的時間,這些操作加起來的時間可能是接近1秒級別的,但由於淘寶的伺服器比較強悍,而且採用了分布式和集群技術,結果比1秒理想一點。但即使有1萬台伺服器,也不能把這個時間稀釋成萬分之一秒,因為,商品只有一種,它有15000個庫存,對應的資料庫記錄只有一行,所有的交易請求都要到這里來處理。
能不能把這15000個拆分成5000個商品並分配到5000台伺服器上呢?那樣不就可以5000台伺服器同時處理了嗎?答案是不能,首先,5000個商品,意味著有5000個商品詳情頁,5000個購買按鈕,這對前期的營銷、引流是個災難。基本上就沒法做引流入口了,顯然這違背了商業管理原則,人為增加了信息混亂程度。其次,天貓魔盒秒殺也不是啥大事,即使按官方標價399元來計算,也就6百萬的交易。如果6百萬的交易要花費那麼大的配套成本,那就太不劃算了。再次,淘寶有十幾億商品,這十幾億商品的展示交易和管理,本來就是分布到上萬台伺服器上去了。沒有必要再把每個商品按庫存拆成多個商品了。
這789人搶到了,還不一定會付款(99積分換天貓魔盒還好一點,不需要去網銀,成本也極低,大部分是會付款的,3999秒殺iPhone5S就不一定,有人可能網銀有問題,有人可能改變主意不想要了),所以就又帶來訂單取消重新恢復庫存的問題。還有想要的消費者們,會認為還有機會,繼續在前台刷一會兒,最終這個秒殺會被熱情的消費者們猛刷30秒到1分鍾。
一分鍾過去了,伺服器終於可以喘口氣了吧?等等,還有超賣,原來,某兩台伺服器在同一毫秒都拿到了鎖,都去減了庫存,15000個庫存,被下了15500個訂單,又得取消一部分訂單。。。如果採用單線程獨占鎖,是可以做到同時只有一個伺服器線程減庫存的,但那樣就對並發高峰的能力就差了好多了。8萬人舉著錢,可能只有8個人能下單成功,這個擁擠狂熱的搶購就要持續10分鍾以上。平時秒個天貓魔盒,10分鍾也就10分鍾吧,雙十一就慘了,收銀台一下子減少了90%,還想做到350億,要麼做夢,要麼再加10倍伺服器和帶寬。所以,商業是不完美的,要在絕對正確和絕對的快速之間做個取捨,保證相對快速又極為正確,允許一定的庫存錯誤和超賣(具體允許多少我也不知道)。
好了,講了這半天淘寶,可以說12306了吧?
我以北京西到深圳北的G71次高鐵為例(這里只考慮南下的方向,不考慮深圳北到北京西的,那是另外一個車次,叫G72),它有17個站(北京西是01號站,深圳北是17號站),3種座位(商務、一等、二等)。表面看起來,這不就是3個商品嗎?G71商務座、G71一等座、G71二等座。大部分輕易噴12306的技術人員(包括某些中等規模公司的專家、CTO)就是在這里栽第一個跟頭的。
實際上,G71有136*3=408種商品(408個SKU),怎麼算來的?請看:
如果賣北京西始發的,有16種賣法(因為後面有16個站),北京西到:保定、石家莊、鄭州、武漢、長沙、廣州、虎門、深圳。。。。都是一個獨立的商品,
同理,石家莊上車的,有15種下車的可能,以此類推,單以上下車的站來計算,有136種票:16+15+14....+2+1=136。每種票都有3種座位,一共是408個商品。
好了,再看出票時怎麼減庫存,由於商務、一等、二等三種座位數是獨立的,庫存操作也是一樣的,下文我就不再提座位的差別的,只討論出發與到達站。另外,下文說的是理論世界的模型,不是說12306的資料庫就是這么設計的。
旅客A買了一張北京西(01號站)到保定東(02號站)的,那【北京西到保定東】這個商品的庫存就要減一,同時,北京西到石家莊、鄭州、武漢、長沙、廣州、虎門、深圳等15個站台的商品庫存也要減一,也就是說,出一張北京到保定東的票,實際上要減16個商品的庫存!
這還不是最復雜的,如果旅客B買了一張北京西(01號站)到深圳北(17號站)的票,除了【北京西到深圳北】這個商品的庫存要減一,北京西到保定東、石家莊、鄭州、武漢、長沙、廣州、虎門等15個站台的商品庫存也要減1,保定東到石家莊、鄭州、武漢、長沙、廣州、虎門、深圳北等15個站台的商品庫存要減1。。。總計要減庫存的商品數是16+15+14+……+1=120個。
當然,也不是每一張票都的庫存都完全這樣實時計算,可以根據往年的運營情況,在黃金周這樣的高峰時段,預先對票做一些分配,比如北京到武漢的長途多一點,保定到石家莊的短途少一點。我沒有證據證實鐵道部這樣做了,但我相信,在還沒有12306網站的時候,鐵道部就有這種人工預分配的策略了。
想像一下,8萬人舉著錢對你高喊:賣給我。你好不容易在錢堆里找到一隻手,拿了他的錢,轉身找120個同事,告訴他們減庫存,而這120個同事也和你一樣被8萬人圍著;也和你一樣,每賣出一個商品要找幾十個人減庫存……這就是12306動態庫存的變態之處。比你平時買東西的任何網站的庫存機制都復雜幾十上百倍。
再說一下搶票插件,機器永遠比人快,當你好不容易從8萬人里突出重圍,來到了櫃台前,你發現,我操,來了10萬根綁著錢的竹竿,而且當有退票出來的時候,你要闖過3層人肉才能接近櫃台,竹竿在8個人身後一伸,錢就到了櫃台前。你低頭看了一眼手機,票就沒了,竹竿卻永遠在那裡伸著,永不低頭,永不眨眼。如果沒有這10萬根竹竿,雖然你很可能還是搶不到票,但不至於沮喪成這樣:我TM為什麼總是手最慢的一個?!!
防機器人搶票,也不是加個圖片驗證碼那麼簡單。我寫過文章系統性分析過,圖片驗證碼有6種機器暴力破解的辦法,搶票插件用的是我說的第三種,OCR識別(光學字元識別——觀察者網注)。Google採用的Wave波形字母已經能比較好地防住機器OCR了,ems.com.cn上的驗證碼就是反面教材,機器OCR成功率接近100%,12306的比ems的圖片驗證碼強一點。不過,驗證碼設置得復雜一點吧,人們要噴:這只是便宜大學生和辦公室白領,農民工連26個字母都認不齊,怎麼搞?搞動畫驗證碼吧,也有人噴,視力不好的人怎麼辦?最後驗證碼搞得太簡單了,皆大歡喜了,其實最高興的是開發搶票插件的公司。
就算採用了機器完全不可能識別的驗證碼,也防不住社會工程學的破解辦法。招募一堆網吧打游戲的青少年朋友,每成功輸入50個驗證碼給1塊錢,或者等值的虛擬貨幣、游戲裝備,我保證想賺這個錢的人數不勝數。這點錢對轉賣車票的利潤而言,是可以接受的成本。有沒有什麼技術可以防住社會工程學的破解辦法呢?能防住網吧青少年的驗證碼只有【2克濃度為3%的U235在大亞灣核電站能發多少KW的電】。
以上討論只是把12306當成和淘寶一樣沒有歷史包袱從零起步的交易系統,實際上,它不是,它後面的票池,還有電話售票、火車站售票、代售點售票等多個傳統渠道要服務。除了客運服務,12306還有全國最大(很可能也是全球最大)的大宗物資貨運系統。
架空政策(包括定價政策、警方打擊黃牛政策、身份驗證政策)談技術,是不可能解決春運搶票困局的,要想讓春運的時候每個人在12306搶票都毫無擁擠感(但不一定能搶到票,鐵路運力擺在那),那就是逼著12306買一大堆伺服器對付春運,春運過去後,成為跟amazon一樣牛逼的雲計算服務商。和逼北京修一條10車道的高速公路去八達嶺長城一個道理。
目前的12306技術上是還有問題,比如,搶票高峰,輸入個身份證號和圖片驗證碼都卡得要死(本人親測),伺服器端繁忙,你瀏覽器端卡什麼呀。
但人家在進步。相信2014年春運的時候,技術已經不再是一票難求的主要問題。在鐵路運力不可能神速增加的情況下,要做到春運更公平地買票,需要停靠政策調整。
下文針對的是春節國慶這種非常暑期。其它時期,大部分線路保持現狀就行了,問題不大,極少部分票源緊張的線路可以按春運處理:
1、拍賣法,價高者得之
當硬座票拍出飛機票價格的時候,相信票就不難買了(可惜就是貴了),也沒有那麼多黃牛了。要說淘寶有什麼能幫12306一下子搞定技術問題的,淘寶的拍賣系統可以幫忙,浙江省高院在淘寶拍賣一年多,成交26億。
可惜這個方法不可能實行。現在的高鐵票價都被媒體和意見領袖噴成啥樣了,何況是拍賣。再說,火車票畢竟是生存之剛需,票價20年來不漲本來就有照顧補貼的成分在裡面,全拍賣可能也是不妥當。
2、抽簽法,運氣好者得之
開車前2個月開放報名,開車前7天抽簽,中途可取消。預存票款,抽不中退款。上傳身份證和正臉自拍照,機器核對。
這樣的話,攔截黃牛的成功率就高很多了,黃牛可以預存票款,可以找到大量真實身份證號,你黃牛再讓每個給你身份證號的人把身份證照片和臉部自拍也給你試試?即使有人真想找黃牛,給身份證照片還是會猶豫一下吧。而且中間手工操作多了很多,黃牛成本提高,還不一定搞得到票。反正都是碰運氣,我想真正的消費者還是會選擇自己先去碰運氣吧。
這個方法實施難度也大,無論怎麼設計抽簽規則,必然有人大叫「有黑幕,不要相信政府」。
開車前7天出抽簽結果,改變行程的人應該在7天前就能決定改還是不改了。沒抽到的也還有時間想別的辦法。當然不一定是7天,15天,10天也可以,具體幾天要有數據模型來算。
3、拍賣+抽簽
軟卧、高鐵商務座等高價位的,拍賣,反正買這個的是經濟能力相對較強的。那就拼誰經濟能力更強吧。
硬座、站票抽簽。
4、憑身份證進站,車票跟發票一樣,是報銷憑證,不是進站憑證;退票後錢進入12306賬戶,不可提現,只可該乘客下次乘車用;黃金周期間,個人賬號最多訂購10張票
這個辦法可以打擊黃牛囤票再轉賣;運行一段時間後,按賬戶余額弄個排行榜就知道誰是黃牛,可惜這個需要車站設備改造配合。
求採納為滿意回答。