訂票的實現思路
同時借助像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案例也是這樣的思路,大家有興趣的可以去看一下我之前錄制的視頻。
查詢余票的實現思路
我覺得余票的查詢的實現相對簡單。雖然對於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的設計思路:1)直接設計了該查詢條件的key,然后快速拿到車次信息,直接返回;這種方式就是要求我們系統已經枚舉了所有車次的所有可能出現的票(區間)的緩存key,相信你一定知道這樣的key是非常多的。2)不是枚舉所有區間,而是把每個車次的每個原子區間(相鄰的兩個站點所連成的直線)的可用票數作為key。這樣,key就非常少了,因為車次假如有10000個,然后每個車次平均15個區間,那也就15W個key而已。當我們要查詢時,只需要把用戶輸入的出發地和目的地之間的所有原子區間的可用票數都查出來,然后比較出最小可用票數的那個原子區間。則這個原子區間的可用票數就是用戶輸入的區間的可用票數了。當然,到這里我提到考慮出發日期。我認為出發日期是用來決定具體是哪個車次聚合根的。同一個車次,不同的日期,對應的聚合根實例是不同的,即便是同一天,也可能有多個車次聚合根,因為有些車次一天有幾班的,比如上午9點發車的一班,下午3點發車的一般。所以,我們也只要把日期也作為緩存key的一部分即可。
