ENode框架Conference案例分析系列之 - 訂單處理減庫存的設計


前言

前面的文章,我介紹了Conference案例的業務、上下文划分、領域模型、架構,以及代碼整體流程。接下來想針對案例中一些重要的場景,分別做進一步的分析。本文想先介紹一下Conference案例的核心業務場景 - 訂單處理減庫存的設計。

下單以及訂單處理流程描述

  1. 下單過程
    • 預訂者瀏覽某個已發布的會議;
    • 進入會議的詳情頁面,該頁面顯示了所有可預訂的座位分類信息;
    • 預訂者選擇好要預訂的座位分類,錄入每個分類的預定數量;
    • 預訂者點擊提交按鈕,提交下單請求到Server端;
  2. Server端訂單處理過程
    • Server端Controller提交處理訂單的命令到分布式消息隊列(EQueue),然后后台的Command Processor就可以消費該命令並異步處理訂單了。核心處理步驟:
      • 生成訂單(初始狀態);
      • 扣減庫存(內部有預扣邏輯);
      • 更新訂單狀態;
    • Server端Controller發送命令后,立即重定向頁面到查單訂單處理結果頁面,該頁面會以輪訓的方式查看訂單處理結果;
  3. 用戶等待訂單處理結果
    • 如果下單成功(庫存足夠),預訂者被導航到支付頁面進行支付;預訂者可以選擇支付,也可以放棄支付;
    • 如果下單失敗(庫存不足),則提示用戶下單失敗,因為庫存不足;
    • 如果輪訓等待超時,則告訴用戶暫時無法知道訂單處理狀態,然后當前頁面繼續定時(5s)輪訓訂單處理結果;
  4. 用戶支付訂單
    • 如果支付成功,則提示預訂者訂單處理完成,交易完成;
    • 如果拒絕支付,則關閉訂單;
    • 如果超過規定時間(15分鍾)未支付,則視作訂單已過期,系統自動回收訂單所預定的座位;
  5. 流程結束;

上面用文字描述了整個下單和訂單處理以及支付的過程,而我們實際關心的核心還是服務端對訂單處理的過程。所以下面我們就看看如何來進行代碼實現。

訂單處理Saga流程圖

Conference案例中,服務端處理訂單是采用CQRS Saga流程的方式實現的。一個Saga流程是一個事件驅動的業務流程,它的周期可能比較長,因為流程中某些步驟需要用戶參與的。上圖描述了服務端處理訂單的正常處理邏輯。為什么說是正常處理邏輯,因為實際的代碼比上面的流程圖還要復雜一點,上面的流程圖中沒有畫出庫存不足、用戶拒絕付款、或者付款超時等情況的處理。我覺得這些特殊的情況,只要讀者自己看一下代碼就能很快理解了。只要我們能夠把正常的邏輯搞清楚,那我們心里就對整個訂單處理的流程有了解了。

上圖中,聚合根之間棕色的箭頭表示Command,藍色的箭頭表示Event。Order Process Manager表示一個無狀態的Saga流程管理器,它負責協調其他有狀態的聚合根,負責整個訂單處理的流程控制邏輯。從代碼表現上來看,它的任務就是響應Event,然后發出下一個Command。然后Order, Conference, Payment三個聚合根分別表示訂單、會議、支付。這三個聚合根分別封裝自己的狀態和業務規則。

訂單處理之減庫存的設計思路

庫存信息在哪里維護

大家都知道,電子商務系統,訂單處理時,核心的環節就是減庫存。那我們首先要思考的問題是,庫存在哪里維護呢?在我看了微軟的Conference案例的代碼后,發現它的庫存信息是在Registration(訂單處理)的上下文中維護的。當ConferenceManagement(會議管理)上下文中,對會議的庫存有修改時,會通過事件異步同步到訂單處理上下文。我在思考它這樣設計的理由是什么,我能想到的唯一理由是,這樣的好處是減庫存時,就只需要在Registration當前的上下文中處理即可,這樣就不需要依賴會議管理上下文了。但代價就是需要從會議管理上下文同步庫存信息。

我個人認為,庫存信息還是應該在會議管理上下文中維護比較合理,因為會議管理上下文的職責就是維護會議的基本信息以及會議的座位類型的實體信息。如果我們的庫存管理沒有獨立為獨立的上下文,那最合理的維護地方就是會議管理上下文。這樣,一份數據就只需要在一個地方維護,不需要同步。然后當訂單處理上下文需要減庫存時,可以通過遠程調用或者異步消息通信的方式來實現上下文之間的交互。

但實際的電商系統,比如像淘寶這種,由於庫存管理也是一塊復雜的業務,所以一般會獨立出一個上下文,叫庫存中心。然后這個庫存中心獨立於商品中心以及訂單中心。當訂單中心要求減庫存時,只需要和庫存中心進行交互即可。這樣的方式,會讓系統的職責更明確,商品中心不需要關心商品的庫存了,只需要關注商品本身的屬性信息即可。然后,本案例由於只是案例,所以沒有獨立出庫存中心,即庫存上下文。所以會議座位的庫存管理放在會議管理上下文中。

我當初看微軟的例子,第一反應就覺得把庫存放到訂單上下文不合理,因為我沒見過這樣的設計。然后我看到會議管理上下文里,它也對會議作為的庫存做了管理,而且是源頭(庫存的第一手數據在會議管理上下文產生),另外,會議管理上下文還會發布會議。所以,這些都讓我意識到,會議管理就是商品中心和庫存中心的結合體。但是讓我費解的是,微軟自己自相矛盾了,居然為了bc之間盡量解耦,居然把庫存信息同步到訂單上下文了。這樣的設計導致代碼非常丑陋,我認為再怎么樣也不能把庫存放到訂單上下文里。所以,最后才有了我的enode的conference這樣的bc的划分的考慮。再聯想到阿里的電商平台,庫存上下文是獨立於訂單上下文的。而我這里的實現,只是偷懶了(因為只是案例),沒有把庫存上下文獨立出來而已。

所以,庫存上下文是合並到訂單上下文比起合並到商品中心上下文更不合理。

庫存怎么預扣

明確了庫存所屬的上下文后,我們接下來思考怎么實現減庫存。減庫存主要的問題是,在並發減庫存的情況下,可能會出現超賣的情況。為了解決超賣的問題,一般主流的做法是采用預扣庫存的方式,類似分布式事務的二階段提交的過程。預扣的意思是先預扣庫存,如果預扣成功,就可以通知用戶下單成功,然后就可以讓用戶去付款了;如果預扣時發現庫存不足,則提示用戶庫存不足。

然后,雖然是預扣,但因為大家同時預扣同一個會議聚合根的座位庫存,所以還是會產生並發問題。但由於我們操作的是同一個聚合根,所以ENode框架幫我們確保不會有並發問題。我們先看看Conference聚合根內部關於座位的庫存管理的設計實現。

如上面的代碼所示,Conference聚合根里聚合了所有的座位類型子實體,每個座位類型維護了座位的名稱、價格、數量;然后Conference聚合根里還維護了所有的預定記錄,這個應該不難理解。MakeReservation方法就是Conference聚合根對外提供預定座位支持的方法。該方法接收一些要預定的項,以及一個預定的ID,表示這次預定是誰(實際上就是訂單ID)要預定。該方法內部的邏輯是:

  1. 判斷當前會議是否沒有發布,如果沒有發布,那是不允許預定的;
  2. 判斷這個預定(reservationId)是否是重復預定,如果重復,也會拋出異常;為什么會出現重復預定,因為訂單處理上下文是通過發送命令的方式來通知Conference進行預定的,而由於是分布式消息隊列(EQueue),所以命令可能會被重復執行。
  3. 檢查預定的座位明細是否為空,如果為空,就認為是無效的預定,拋異常;
  4. 接下來就是循環處理每個預定項,先檢查預定項本身需要預定的數量是否無效(小於等於0),如果無效,則拋出異常;再從Conference聚合根里找到當前要預定的座位類型子實體;然后計算當前的座位類型是否有足夠的可用庫存,GetTotalReservationQuantity方法就是計算當前該座位類型總共已經預定的總數。如果庫存不足,則直接拋出異常。當然這里直接拋出異常可能還是太草率了一點。因為真正的電子商務系統,應該會明確提示用戶,哪些商品庫存不足,是否要修改訂單只購買剩余的庫存。本案例為了讓代碼不會太復雜,所以簡化了功能。只要被預定的座位類型出現一個庫存不足,就認為下單失敗了。
  5. 當所有的預定項都處理完成后,就可以產生“已預定”的領域事件了。注意,這里我們產生事件的時候,同時把當前每個座位類型剩余的庫存數量也放在領域事件里了。這樣的好處是,當Q端的Event Handler在更新Conference的讀庫時,不需要再計算了,直接用Update語句更新DB即可。這個設計大家可以參考下,這樣的設計,體現了Domain中封裝了一切數據更新的業務規則判斷和邏輯處理,然后通過事件的方式通知Domain外部當前事件發生后,聚合根的當前狀態(一定是第一手數據,不會是臟數據)是什么。這樣外部的Event Handler的邏輯就非常簡單了,都只需要簡單的用Update語句更新DB即可(不用動腦子,呵呵)。

並發問題的處理

Domain不會考慮並發這種技術問題,它只關心自己的業務規則和數據一致性,完全從業務角度來寫代碼。我們可以看到Conference聚合根里封裝了很多的規則和邏輯。然后Conference聚合根產生的Event持久化到EventStore時的並發問題,ENode框架會幫我們解決,應用開發者不用擔心了。如果大家關心是怎么解決的,可以去看一下ENode我以前寫過的一些介紹,核心思路是樂觀並發控制(通過聚合根版本號)+ 自動重試的機制,這里我就不展開了。

通過上面的設計,我們知道每次預扣時總是會判斷當前可用的庫存,並且已經考慮了其他已經預扣了的訂單;這就從業務邏輯上保證了不會出現超賣;然后ENode框架解決了並發問題,所以最后我們可以確保一定不會出現超賣的情況。

用戶付款后怎么真正減庫存

當預扣成功后,用戶就會去付款,假如付款成功了,那系統就會自動提交之前的預扣記錄,做真正的減庫存。我們來看看邏輯是怎么樣的:

CommitReservation方法是Conference聚合根用來提供支持提交減庫存的方法。它接收一個要提交減庫存的reservationId,通過該ID,先找到之前它預定的所有預定項,然后產生一個事件,事件中包含每個預定項所對應的座位類型的扣除后的庫存數量,最后產生領域事件。然后聚合根內部會響應領域事件,更新聚合根自己的狀態。我們在Commit階段是不用擔心數據有什么問題的,因為肯定是之前預扣過了,只要預扣記錄存在,那就可以放心的做減庫存邏輯的。這是我們通過業務上的2PC協議保證的。

代碼很直接,就是先刪除預定記錄,並把預定記錄的每個明細對應的座位類型的庫存更新即可。然后,我們的讀庫的更新也是這樣的邏輯,只是更新的是讀庫DB而已。

結束語

好了,本文基本把訂單處理的核心環節減庫存講了一下,本來還想再結合訂單狀態的變更講一下訂單狀態在這個過程中是如何變化的。但由於今天時間比較完了,不准備講了。我在前面的領域模型的介紹中,已經基本講了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM