淺談12306核心模型設(shè)計思路和架構(gòu)設(shè)計
當(dāng)前位置:點晴教程→知識管理交流
→『 技術(shù)文檔交流 』
【編者按】2015年看到一個系列選題,第一篇是《技術(shù)揭秘12306改造(一):尖峰日PV值297億下可每秒出票1032張》。一年過去,應(yīng)該又有很多變化。下述是一位技術(shù)專家——何望老師在其博客的分享,其對12306系統(tǒng)核心領(lǐng)域模型設(shè)計進行了深入討論,并希望利用業(yè)余時間基于ENode實現(xiàn)一套12306的核心功能,比如余票查詢、訂票的功能。這些系列內(nèi)容建議相互印證來看。 前言春節(jié)期間,無意中看到一篇文章,文章中講到12306的業(yè)務(wù)復(fù)雜度遠(yuǎn)遠(yuǎn)比淘寶天貓這種電商網(wǎng)站要復(fù)雜。后來自己想想,也確實如此。所以,很想挑戰(zhàn)一下12306這個系統(tǒng)的核心領(lǐng)域模型的設(shè)計。一般的電商網(wǎng)站,購買都是基于商品的概念,每個商品有一定量的庫存,用戶的購買行為是針對商品的。當(dāng)用戶發(fā)起購買行為時,系統(tǒng)只需要生成訂單并對用戶要購買的商品減庫存即可。但是,12306就不是那么簡單了,具體復(fù)雜在哪里,我下面會進一步分析。 另外一個讓我寫這篇文章的原因,是我發(fā)現(xiàn)也許是否是因為目前12306的核心領(lǐng)域模型設(shè)計的不夠好,導(dǎo)致用戶購票時要處理的業(yè)務(wù)邏輯異常復(fù)雜,維護數(shù)據(jù)一致性的難度也幾百倍的上升,同時面對高并發(fā)的訂票也難以支持很高的TPS。我覺得,越是復(fù)雜的業(yè)務(wù),就越要重視業(yè)務(wù)分析,重視領(lǐng)域模型的抽象和設(shè)計。如果不假思索,憑以往經(jīng)驗行事,則很可能會被以往的設(shè)計經(jīng)驗先入為主,陷入死胡同。我發(fā)現(xiàn)技術(shù)人員往往更注重技術(shù)層面的解決方案,比如一上來就分析如何集群、如何負(fù)載均衡、如何排隊、如何分庫分表、如何用鎖,如何用緩存等技術(shù)問題,而忽略了最根本的業(yè)務(wù)層面的思考,如分析業(yè)務(wù)、領(lǐng)域建模。我認(rèn)為越是復(fù)雜的業(yè)務(wù)系統(tǒng),則越要設(shè)計一個健壯的領(lǐng)域模型。如果一個系統(tǒng)的架構(gòu)我們設(shè)計錯了,還有補救的余地,因為架構(gòu)最終沉淀的只是代碼,調(diào)整架構(gòu)即可(一個系統(tǒng)的架構(gòu)本身就是不斷演進的);而如果領(lǐng)域模型設(shè)計錯了,那要補救的代價是非常大的,因為領(lǐng)域模型沉淀的是數(shù)據(jù)結(jié)構(gòu)及其對應(yīng)的大量數(shù)據(jù),對任何一個大型系統(tǒng),要改核心領(lǐng)域模型都是成本非常高的。 本文的重點不是在如何解決高并發(fā)的問題,而是希望從業(yè)務(wù)角度去分析,12306的理想模型應(yīng)該是怎么樣的。網(wǎng)上目前談12306的文章貌似都是千篇一律的只談技術(shù),不談業(yè)務(wù)分析和如何建模的。所以我想寫一下自己的設(shè)計和大家交流學(xué)習(xí)。 需求概述12306這個系統(tǒng),核心要解決的問題是網(wǎng)上售票。涉及到2個角色使用該系統(tǒng):用戶、鐵道部。用戶的核心訴求是查詢余票、購票;鐵道部的核心訴求是售票。購票和售票其實是一個場景,對用戶來說是購票,對鐵道部來說是售票。因此,我們要設(shè)計一個在線的網(wǎng)站系統(tǒng),解決用戶的查詢余票、購票,以及鐵道部的售票這3個核心訴求??雌饋?,這3個場景都是圍繞火車票展開的。 查詢余票:用戶輸入出發(fā)地、目的地、出發(fā)日三個條件,查詢可能存在的車次,用戶可以看到每個車次經(jīng)過的站點名稱,以及每種座位的余票數(shù)量。 購票:購票分為訂票和付款兩個階段,本文重點分析訂票的模型設(shè)計和實現(xiàn)思路。 其實還有很多其他的需求,比如給不同的車次設(shè)定銷售座位數(shù)配額,以及不同的區(qū)段設(shè)置不同的限額。但相比前面兩個需求來說,我覺得這個需求相對次要一些。 需求分析確實,12306也是一個電商系統(tǒng),而且看起來商品就是票了。因為如果把一張票看成是一個商品,那購票就類似于購買商品,然后每張票都有庫存,商品也有庫存的概念。但是如果我們仔細(xì)想想,會發(fā)現(xiàn)12306要復(fù)雜很多,因為我們無法預(yù)先確定好所有的票,如果非要確定,那只能通過窮舉法了。 我們以北京西到深圳北的G71車次高鐵為例(這里只考慮南下的方向,不考慮深圳北到北京西的,那是另外一個車次,叫G72),它有17個站(北京西是01號站,深圳北是17號站),3種座位(商務(wù)、一等、二等)。表面看起來,這不就是3個商品嗎?G71商務(wù)座、G71一等座、G71二等座。大部分輕易噴12306的技術(shù)人員(包括某些中等規(guī)模公司的專家、CTO)就是在這里栽第一個跟頭的。實際上,G71有136*3=408種商品(408個SKU),怎么算來的?如下: 如果賣北京西始發(fā)的,有16種賣法(因為后面有16個站),北京西到:保定、石家莊、鄭州、武漢、長沙、廣州、虎門、深圳。。。。都是一個獨立的商品,同理,石家莊上車的,有15種下車的可能,以此類推,單以上下車的站來計算,有136種票:16+15+14....+2+1=136。每種票都有3種座位,一共是408個商品。 為了方便后面的討論,我們先明確一下票是什么? 一張票的核心信息包括:出發(fā)時間、出發(fā)地、目的地、車次、座位號。持有票的人就擁有了一個憑證,該憑證表示持有它的人可以坐某個車次的某個座位號,從某地到某地。所以,一張票,對用戶來說是一個憑證,對鐵道部來說是一個承諾;那對系統(tǒng)來說是什么呢?不知道。這就是我們要分析業(yè)務(wù),領(lǐng)域建模的原因,我們再繼續(xù)思考吧。 明白了票的核心信息后,我們再看看G71這個車次的高鐵,可以賣多少張票? 討論前先說明一下,一輛火車的物理座位數(shù)(站票也可以看成是一種座位,因為站票也有數(shù)量配額)不等于可用的最大配合。所有的物理座位不可能都通過12306網(wǎng)站來銷售,而是只會銷售一部分,比如40%。其余的還是會通過線下的方式銷售。不僅如此,可能有些站點上車的人會比較多,有些比較少,所以我們還會給不同的區(qū)間配置不同的限額。比如D31北京南至上海共有765張,北京南有260張,楊柳青有80張,泰安有76張。如果楊柳青的80張票售完就會顯示無票,就算其他站有票也會顯示無票的。每個車次肯定會有各種座位的配額和限額的配置的,這種配置我目前無法預(yù)料,但我已經(jīng)把這些規(guī)則都封裝近車次聚合根里了,所有的配置策略都是基于座位類型、站點、區(qū)間配置的。關(guān)于票的配置抽象出來,我覺得主要有兩種:1)某個區(qū)段最多允許出多少張;2)某個區(qū)段最少允許出多少張。當(dāng)用戶訂票時,把用戶指定的區(qū)段和這兩種配置條件進行比較,兩個條件都滿足,則可以出票。不滿足,則認(rèn)為無票了。下面舉個例子: ABCDEFG,這是所有站點。座位總配額是100,假設(shè)B站點上車,E站下車的人比較少,那我們就可以設(shè)定BE這個區(qū)段最多只能出10張票。所以,只要是用戶的訂票是在這個區(qū)段內(nèi)的,就最多出10張。再比如,一列車次,總共100個座位配額,希望全程票最少滿足80張,那我們只要給AG這個區(qū)段設(shè)定最少80張。那任何訂票請求,如果是子區(qū)間的,就不能超過100-80,即20張。這兩種條件必須同時滿足,才允許出票。 但是,不管如何做配額和限額,我們總是針對某個車次進行配置,這些配置只是車次內(nèi)部售票時的一些額外的判斷條件(業(yè)務(wù)規(guī)則),不影響車次模型的核心地位和對外暴露的功能。所以,為了本文討論的清楚起見,我后續(xù)的討論都不涉及配額和限額的問題,而是認(rèn)為任何區(qū)段都可以享受火車最大的物理座位數(shù)。 并且,為了討論問題方便,我們減少一些站點來討論。假設(shè)某個車次有A,B,C,D四個站點。那001這個人購買了A,B這個區(qū)間,系統(tǒng)會分配給001一個座位x;但是因為001坐到B站點后會下車,所以相當(dāng)于x這個座位又空出來了,也就是說,從B站點開始,系統(tǒng)又可以認(rèn)為x這個座位是可用的。所以,我們得出結(jié)論:同一個座位,其實可以同時出售AB,BC這兩張票。通過這個簡單的分析,我們知道,一列火車雖然只有有限的座位數(shù),比如1000個座位。但可以賣出的票遠(yuǎn)遠(yuǎn)不止1000個。還是以A,B,C,D四個站點為例,假如火車總共有1000個座位,那AB可以賣1000張,BC也可以賣1000張,同樣,CD也可以賣1000張。也就是說,理論上最多可以賣出3000張票。但是如果換一種賣法,所有人都是買ABCD的票,也就是說所有的票都是經(jīng)過所有站點的,那就是最多只能賣出1000張票了。而實際的場景,一定是介于1000到3000之間。然后實際的G71這個車次,有17個站,那到底可以賣出多少個票,大家應(yīng)該可以算了吧。理論上這17個站中的任意兩個站點之間所形成的線段,都可以出售為一張票。我數(shù)學(xué)不好,算不太清楚,麻煩有數(shù)學(xué)好的人幫我算算,呵呵。 通過上面的分析,我們知道一張票的本質(zhì)是某個車次的某一段區(qū)間(一條線段),這個區(qū)間包含了若干個站點。然后我們還發(fā)現(xiàn),只要區(qū)間不重疊,那座位就不會發(fā)生競爭,可以被回收利用,也就是說,可以同時預(yù)先出售。 另外,經(jīng)過更深入的分析,我們還發(fā)現(xiàn)區(qū)間有4種關(guān)系:1)不重疊;2)部分重疊;3)完全重疊;4)覆蓋;不重疊的情況我們已經(jīng)討論過了,而覆蓋也是重疊的一種。所以我們發(fā)現(xiàn)如果重疊,比如有兩個區(qū)間發(fā)生重疊,那重疊部分的區(qū)間(可能夸一個或多個站點)是在爭搶座位的。因為假設(shè)一列火車有100個座位,那每個原子區(qū)間(兩個相鄰站點的連線),最多允許重疊99次。 所以,經(jīng)過上面的分析,我們知道了一個車次能夠出售一張車票的核心業(yè)務(wù)規(guī)則是什么?就是:這張車票所包含的每個原子區(qū)間的重疊次數(shù)加1都不能超過車次的總座位數(shù),實際上重疊次數(shù)+1也可以理解為線段的厚度。 模型設(shè)計上面我分析了一下票的本質(zhì)是什么。那接下來我們再來看看怎么設(shè)計模型,來快速實現(xiàn)購票的需求,重點是怎么設(shè)計商品聚合以及減庫存的邏輯。 傳統(tǒng)電商的思路如果按照普通電商的思路,把票(站點區(qū)間)設(shè)計為商品(聚合根),然后為票設(shè)計庫存數(shù)量。我個人覺得是很糟糕的。因為一方面這種聚合根非常多(上面的G71就有408個);另一方面,即便枚舉出來了,一次購票也一定會影響非常多其他聚合根的庫存數(shù)量(只要被部分或全部重疊的區(qū)間都受影響)。這樣的一次訂單處理的復(fù)雜度是難以評估的。而且這么多聚合根的更新要在一個事務(wù)里,這不是為難數(shù)據(jù)庫嗎?而且,這種設(shè)計必然帶來大量的事務(wù)的并發(fā)沖突,很可能導(dǎo)致數(shù)據(jù)庫死鎖??傊?,我認(rèn)為這種是典型的由于領(lǐng)域模型的設(shè)計錯誤,導(dǎo)致并發(fā)沖突高、數(shù)據(jù)持久化落地困難?;蛘呷绻鉀Q并發(fā)問題,只能排隊單線程處理,但是仍然解決不了要在一個事務(wù)里修改大量聚合根的尷尬局面。聽說12306是采用了Pivotal Gemfire這種高大上的內(nèi)存數(shù)據(jù)庫,我對這個不太了解。我不可想象要是不使用內(nèi)存數(shù)據(jù)庫,他們要怎么實現(xiàn)車次內(nèi)的票之間的數(shù)據(jù)強一致性(就是保證所有出售的票都是符合上面討論的業(yè)務(wù)規(guī)則的)?所以,這種設(shè)計,我個人認(rèn)為是思維定勢了,把火車票看成是普通電商的商品來看待。所以,我們有時做設(shè)計又要依賴于經(jīng)驗,又要不能被以往經(jīng)驗所束縛,真的不容易,關(guān)鍵還是要根據(jù)具體的業(yè)務(wù)場景多多深入分析,盡量分析抽象出問題的本質(zhì)出來,這樣才能對癥下藥。那是否有其他的設(shè)計思路呢? 我的思路聚合設(shè)計通過上面的分析我們知道,其實任何一次購票都是針對某個車次的,我認(rèn)為車次是負(fù)責(zé)處理訂票的聚合根。我們看看一個車次包含了哪些信息?一個車次包括了:1)車次名稱,如G71;2)座位數(shù),實際座位數(shù)會分類型,比如商務(wù)座20個,一等座200個;二等座500個;我們這里為了簡化問題,可以暫時忽略類型,我認(rèn)為這個類型不影響核心的模型的設(shè)計決策。需要格外注意的是:這里的座位數(shù)不要理解為真實的物理座位數(shù),很有可能比真實的座位數(shù)要少。因為我們不可能把一個車次的所有座位都在網(wǎng)上通過12306來出售,而是只出售一部分,具體出售多少,要由工作人員人工指定。3)經(jīng)過的站點信息(包括站點的ID、站點名稱等),注意:車次還會記錄這些站點之間的順序關(guān)系;4)出發(fā)時間;看過GRASP九大模式中的信息專家模式的同學(xué)應(yīng)該知道,將職責(zé)分配給擁有執(zhí)行該職責(zé)所需信息的類。我們這個場景,車次具有一次出票的所有信息,所以我們應(yīng)該把出票的職責(zé)交給車次。另外學(xué)過DDD的同學(xué)應(yīng)該知道,聚合設(shè)計有一個原則,就是:聚合內(nèi)強一致性,聚合之間最終一致性。經(jīng)過上面的分析,我們知道要產(chǎn)生一張票,其實要影響很多和這個票對應(yīng)的線段相交的其他票的可用數(shù)量。因為所有的站點信息都在車次聚合內(nèi)部,所以車次聚合內(nèi)部自然可以維護所有的原子區(qū)間,以及每個原子區(qū)間的可用票數(shù)(相當(dāng)于是庫存數(shù))。當(dāng)一個原子區(qū)間的可用票數(shù)為0的時候,意味著火車針對這個區(qū)間的票已經(jīng)賣完了。所以,我們完全可以讓車次這個聚合根來保證出票時對所有原子區(qū)間的可用票數(shù)的更新的強一致性。對于車次聚合根來說,這很簡單,因為只是幾次簡單的內(nèi)存操作而已,耗時可以忽略。一列火車假如有ABCD四個站點,那原子區(qū)間就是3個。對于G71,則是16個。 怎么判斷是否能出票基于上面的聚合設(shè)計,出票時扣減庫存的邏輯是: 根據(jù)訂單信息,拿到出發(fā)地和目的地,然后獲取這段區(qū)間里的所有的原子區(qū)間。然后嘗試將每個原子區(qū)間的可用票數(shù)減1,如果所有的原子區(qū)間都夠減,則購票成功;否則購票失敗,提示用戶該票已經(jīng)賣完了。是不是很簡單呢?知道了出票的邏輯,那退票的邏輯也就很簡單了,就是把這個票的所有原子區(qū)間的可用票數(shù)加1就OK了。如果我們從線段的厚度的角度去考慮,那出票時,每個原子區(qū)間的厚度就是+1,退票時就是減一。就是相反的操作,但本質(zhì)是一樣的。 所以,通過這樣的思路,我們將一次訂票的處理控制在了一個聚合根里,用聚合根內(nèi)的強一致性的特性保證了訂票處理的強一致性,同時也保證了性能,免去了并發(fā)沖突的可能性。傳統(tǒng)電商那種把票單做類似商品的核心聚合根的設(shè)計,我當(dāng)時第一眼看到就覺得不妥。因為這違背了DDD強調(diào)的強一致性應(yīng)該由聚合根來保證、聚合根之間的最終一致性通過Saga來保證的原則。 還有一個很重要的概念我想說一下我的看法,就是座位和區(qū)間的關(guān)系。因為有些朋友和我講,考慮座位號的問題,雖然都能減1,座位號也必須是同一個。我覺得座位是全局共享的,和區(qū)段無關(guān)(也許我的理解完全有誤,請大家指正)。座位是一個物理概念,一個用戶成功購買了一張票后,座位就會少一個,一張票唯一對應(yīng)一個座位,但是一個座位有可能會對應(yīng)多張票;而區(qū)間是一個邏輯上的概念,區(qū)間的作用有兩個:1)表示票的出發(fā)地和目的地;2)記錄票的可用數(shù)額。如果區(qū)間能連通(即該區(qū)間內(nèi)的每個原子區(qū)間的可用數(shù)額都大于0),則表示允許擁有一個座位。所以,我覺得座位和票(區(qū)間)是兩個維度的概念。 如何為票分配座位我覺得車次聚合根內(nèi)部應(yīng)該維護所有該車次已經(jīng)售出的票,已經(jīng)出售的票的的本質(zhì)是區(qū)間和座位的對應(yīng)關(guān)系。系統(tǒng)處理訂票時,用戶提交過來的是一段區(qū)間。所以,系統(tǒng)應(yīng)該做兩個事情:
當(dāng)?shù)玫揭粋€可用座位后,就可以生成一張票了,然后保存這個票到車次聚合根內(nèi)部即可。下面舉個例子: 假設(shè)現(xiàn)在的情況是座位有3個,站點有4個 票的賣法1: 票的賣法2: 所以,從上面的分析我們也知道選座位的算法該怎么寫了,就是采用優(yōu)先回收利用座位的算法。我認(rèn)為不管我們這里怎么設(shè)計算法,都不影響大局,因為這一切都只發(fā)生在車次聚合根內(nèi)部,這就是預(yù)先設(shè)計好聚合根,明確出票職責(zé)在哪個對象上的好處。 模型分析總結(jié)
通過這樣的模型設(shè)計,我們可以確保一次出票處理只會在一個車次聚合根內(nèi)進行。這樣的好處是:
架構(gòu)設(shè)計(非本文重點,沒興趣的朋友可以略過)我覺得12306這樣的業(yè)務(wù)場景,非常適合使用CQRS架構(gòu);因為首先它是一個查多寫少、但是寫的業(yè)務(wù)邏輯非常復(fù)雜的系統(tǒng)。所以,非常適合做架構(gòu)層面的讀寫分離,即采用CQRS架構(gòu)。而且應(yīng)該使用數(shù)據(jù)存儲也分離的CQRS。這樣CQ兩端才可以完全不需要顧及對方的問題,各自優(yōu)化自己的問題即可。我們可以在C端使用DDD領(lǐng)域模型的思路,用良好設(shè)計的領(lǐng)域模型實現(xiàn)復(fù)雜的業(yè)務(wù)規(guī)則和業(yè)務(wù)邏輯。而Q端則使用分布式緩存方案,實現(xiàn)可伸縮的查詢能力。 訂票的實現(xiàn)思路同時借助像ENode這樣的框架,我們可以實現(xiàn)in-memory + Event Sourcing的架構(gòu)。Event Sourcing技術(shù),可以讓領(lǐng)域模型的所有狀態(tài)修改的持久化統(tǒng)一起來,本來要用ORM的方式保存聚合根最新狀態(tài)的,現(xiàn)在只需要簡單的通用的方式保存一個事件即可(一次訂票只涉及一個車次聚合根的修改,修改只產(chǎn)生一個事件,只需要持久化一個事件(一個JSON串)即可,保證了高性能,無須依賴事務(wù),而且通過ENode可以解決并發(fā)問題)。我們只要保存了聚合根每次變化的事件(事件的結(jié)構(gòu)怎么設(shè)計,本文不做多的介紹了,大家可以思考下),就相當(dāng)于保存了聚合根的最新狀態(tài)。而正是由于Event Sourcing技術(shù)的引入,讓我們的模型可以一直存活在內(nèi)存中,即可以使用in-memory技術(shù)。不要小看in-memory技術(shù),in-memory技術(shù)在某些方面對提高命令的處理性能非常有幫助。比如就以我們車次聚合根處理出票的邏輯,假設(shè)某個車次有大量的命令發(fā)送到分布式消息隊列,然后有一臺機器訂閱了這個隊列的消息,然后這臺機器處理這個車次的訂票命令時,由于這個車次聚合根一直在內(nèi)存,所以就省去了每次要去數(shù)據(jù)庫取出聚合根的步驟,相當(dāng)于少了一次數(shù)據(jù)庫IO。這樣的好處是,因為一個車次能夠真正出售的票是有限的,因為座位就那么幾個,比如就1000個座位,估計一般正常情況也就出個2000個左右的票吧(具體能出多少張票要取決于區(qū)間的相交程度,上面分析過)。也就是說,這個聚合根只會產(chǎn)生2000個事件,也就是說只會有2000個訂票命令的處理是會產(chǎn)生事件,并持久化事件;而其余的大量命令,因為車次在內(nèi)存計算后發(fā)現(xiàn)沒有余票了,就不會做任何修改,也不會產(chǎn)生領(lǐng)域事件,這樣就可以直接處理下一個訂票命令了。這樣就可以大大提高處理訂票命令的性能。 另外一個問題我覺得還需要提一下,因為用戶訂票成功后,還需要付款。但用戶有可能不去付款或者沒有在規(guī)定的時間內(nèi)完成付款。那這種情況下,系統(tǒng)會自動釋放該用戶之前訂購的票。所以基于這樣的需求,我們在業(yè)務(wù)上需要支持業(yè)務(wù)級別的2pc。即先預(yù)扣庫存,也就是先占住這張票一定時間(比如15分鐘),然后付款成功后再真實給你這張票,系統(tǒng)做真正的庫存修改。通過這樣的預(yù)扣處理,可以保證不會出現(xiàn)超賣的情況。這個思路其實和傳統(tǒng)電商比如淘寶這樣的系統(tǒng)類似,我就不多展開了,我之前寫的Conference案例也是這樣的思路,大家有興趣的可以去看一下我之前錄制的視頻。 查詢余票的實現(xiàn)思路我覺得余票的查詢的實現(xiàn)相對簡單。雖然對于12306來說,查詢的請求占了80%,提交訂單的請求只占20%。但查詢由于對數(shù)據(jù)沒有修改,所以我們完全可以使用分布式緩存來實現(xiàn)。我們只需要精心設(shè)計好緩存的key即可;緩存key的多少要看成本,如果所有可能的查詢都設(shè)計對應(yīng)的key,那時間復(fù)雜度為1,查詢性能自然高;但代價也大,因為key多了。如果想key少一點,那查詢的復(fù)雜度自然要上去一點。所以緩存設(shè)計無非就是空間換時間的思路。然后,緩存的更新無非就是:自動失效、定時更新、主動通知3種。通過CQRS架構(gòu),由于CQ兩端是事件驅(qū)動的,當(dāng)C端有任何狀態(tài)變化,都會產(chǎn)生對應(yīng)的事件去通知Q端,所以我們幾乎可以做到Q端的準(zhǔn)實時更新。 同時由于CQ兩端的完全解耦,Q端我們可以設(shè)計多種存儲,如數(shù)據(jù)庫和緩存(Redis等);數(shù)據(jù)庫用于線下維護關(guān)系型數(shù)據(jù),緩存用戶實時查詢。數(shù)據(jù)庫和緩存的更新速度相互不受影響,因為是并行的。對同一個事件,可以10臺機器負(fù)責(zé)更新緩存,100臺機器負(fù)責(zé)更新數(shù)據(jù)庫。即便數(shù)據(jù)庫的更新很慢,也不會影響緩存的更新進度。這就是CQRS架構(gòu)的好處,CQ的架構(gòu)完全不同,且我們隨時可以重建一種新的Q端存儲。不知道大家體會到了沒有? 關(guān)于緩存key的設(shè)計,我覺得主要從查詢余票時傳遞的信息來考慮。12306的關(guān)鍵查詢是:出發(fā)地、目的地、出發(fā)日期三個信息。我覺得有兩種key的設(shè)計思路:1)直接設(shè)計了該查詢條件的key,然后快速拿到車次信息,直接返回;這種方式就是要求我們系統(tǒng)已經(jīng)枚舉了所有車次的所有可能出現(xiàn)的票(區(qū)間)的緩存key,相信你一定知道這樣的key是非常多的。2)不是枚舉所有區(qū)間,而是把每個車次的每個原子區(qū)間(相鄰的兩個站點所連成的直線)的可用票數(shù)作為key。這樣,key就非常少了,因為車次假如有10000個,然后每個車次平均15個區(qū)間,那也就15W個key而已。當(dāng)我們要查詢時,只需要把用戶輸入的出發(fā)地和目的地之間的所有原子區(qū)間的可用票數(shù)都查出來,然后比較出最小可用票數(shù)的那個原子區(qū)間。則這個原子區(qū)間的可用票數(shù)就是用戶輸入的區(qū)間的可用票數(shù)了。當(dāng)然,到這里我提到考慮出發(fā)日期。我認(rèn)為出發(fā)日期是用來決定具體是哪個車次聚合根的。同一個車次,不同的日期,對應(yīng)的聚合根實例是不同的,即便是同一天,也可能有多個車次聚合根,因為有些車次一天有幾班的,比如上午9點發(fā)車的一班,下午3點發(fā)車的一般。所以,我們也只要把日期也作為緩存key的一部分即可。 總結(jié)本文完全是憑自己對12306這個網(wǎng)站的核心業(yè)務(wù)的簡單思考而得到的一些設(shè)計結(jié)果。如果真正的DDD領(lǐng)域建模,更多的是要和業(yè)務(wù)一線的工作人員、領(lǐng)域?qū)<疫M行深入溝通,才能更深入的了解該領(lǐng)域內(nèi)的業(yè)務(wù)知識,從而才能設(shè)計出更靠譜的領(lǐng)域模型和架構(gòu)設(shè)計。我本人非常慚愧因為沒有上12306買過火車票,家離的比較近,就算要買也是家人給我買:)所以,本文所分享的內(nèi)容難免是紙上談兵。但我覺得12306這個系統(tǒng)的業(yè)務(wù)確實比傳統(tǒng)的電商系統(tǒng)要復(fù)雜,且并發(fā)又這么高。所以,我覺得這個系統(tǒng)真的很值得大家重視模型的設(shè)計,而不只是只關(guān)注技術(shù)層面的實現(xiàn)。 該文章在 2016/2/23 16:12:56 編輯過 |
關(guān)鍵字查詢
相關(guān)文章
正在查詢... |