這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
序言
筆者在《軟體設計的演變過程》一文中,將通訊系統軟體的DDD分層模型最終演化為五層模型,即調度層(Schedule)、事務層(Transaction DSL)、環境層(Context)、領域層(Domain)和基礎設施層(Infrastructure),我們簡單回顧一下:
ddd-layer-with-dci-dsl.png
- 調度層:維護UE的狀態模型,只包括業務的本質狀態,將接收到的訊息派發給事務層。
- 事務層:對應一個商務程序,比如UE Attach,將各個同步訊息或非同步訊息的處理組合成一個事務,當事務失敗時,進行復原。當事務層收到調度層的訊息後,委託環境層的Action進行處理。
- 環境層:以Action為單位,處理一條同步訊息或非同步訊息,將Domain層的領域對象cast成合適的role,讓role互動起來完成商務邏輯。
- 領域層:不僅包括領域對象及其之間關係的建模,還包括對象的角色role的顯式建模。
- 基礎實施層:為其他層提供通用的技術能力,比如訊息通訊機制、對象持久化機制和通用的演算法等
本文將聚焦於事務層,主要討論事務模型,代碼抽象層次和商務程序圖一一對應。
同步模型
毫無疑問,非同步模型是複雜的。但在管理域的組件中,對即時性和效能並沒有極致的要求,同時協程(比如,Goroutine)非常輕量級,所以使用同步模型是一種非常聰明且簡單的處理方式,如所示:
synchronous-model.png
在一個同步模型裡,一個系統一旦發出一個請求訊息,並需要等待其應答,則當前協程就會進入休眠態,直到應答訊息來臨或逾時為止。協程可以看做是使用者態輕量級的線程,佔用資源非常少,當前系統同時可以有成百上千個協程運行。
假定Action是一條同步訊息的互動,那麼業務的流程圖就對應一個Action序列。
事務
事務(Transaction,簡寫為Trans)一詞來源於資料處理的概念,下面是Wikipedia 對事務的定義:
In computer science, transaction processing is information processing thatis divided into individual, indivisible operations, called transactions. Each transaction must succeed or fail as a complete unit; it cannot remain in an intermediate state.
一般情況下,一個單一情境的使用者流程圖就對應一個事務,而事務則由一個Action序列組成。
transaction.png
從S1到S2的一次同步請求處理過程中,站在S1的視角是一個Action,而站在S2的視角卻是一個事務。
事務過程式控制制
基礎資料結構
TransInfo
TransInfo是事務模型中一個非常重要的資料結構,用於事務執行過程中的資料傳遞,比如事務層注入到環境層的資料,Action之間串聯的資料。
S1Obj
當前系統為S2,當收到來自S1的同步請求時,S2啟動一個協程處理該請求。當該協程調用到調度層後,需要先初始化資料變數TransInfo和建立領域對象S1Obj,然後將它們注入到事務對象,最後執行事務。如果事務執行失敗,則進行復原。
func scheduleS1ReqTrans(req []byte) error { transInfo := &context.TransInfo{Names: make([]string, 0)} S1Obj := s1obj.CreateS1Obj(req) s1ReqTrans := trans.NewS1ReqTrans() err = s1ReqTrans.Exec(S1Obj, transInfo) if err != nil { s1ReqTrans.RollBack(S1Obj, transInfo) } return err}
Fragment
從語義層次上看,一個Fragment是一個流程片段。
從代碼層次上看,一個Fragment是一個interface。
type Fragment interface { Exec(s1Obj *s1obj.S1Obj, transInfo *context.TransInfo) error RollBack(s1Obj *s1obj.S1Obj, transInfo *context.TransInfo)}
Action
Action是一條同步訊息的互動,在環境層定義,是提供給事務層的原子操作,是一個Fragment,實現了Fragment介面,具體實現和業務緊密相關。
Procedure
Procedure是多條關係緊密的同步訊息的互動,在事務層定義,是比Action更大的複用單元,是一個Fragment,實現了Fragment介面。
Procedure本身又是一個由Action或Procedre組成的序列,其中Action是葉子節點,Procedure是中間節點,所以Procedre是一棵多叉樹。
一個通用的Procedure的代碼定義如下:
type Procedure struct { fragments []Fragment}
建立一個具體的Procedure的代碼如下:
func newA1Procedure() *Procedure { a1Procedure := &Procedure{ fragments: []Fragment{ new(context.Action11), newA2Procedure(), new(context.Action12)}} return a1Procedure}
Procedure的執行很簡單,直接調用事務層封裝的原語for_each_fragments即可。
repeat
從語義層次來看,repeat用來修飾Action或Procedure,說明該Action或Procedure可以執行多次,並且至少執行一次。
從代碼層次來看,repeat也是一個Fragment,因為其實現了該介面。
綜上,repeat在本質上是對Action或Procedure的封裝,同時也有Fragment的行為。
repeat的代碼定義如下:
type repeat struct { fragment Fragment}
repeat的執行次數是動態確定的,即由上一個Action寫入TransInfo。
有了repeat後,我們可以定義一個事務如下:
func NewS1Trans() *Transaction { s1Trans := &Transaction{ fragments: []Fragment{ new(context.Action1), new(context.Action2), repeat{fragment:newA1Procedure()}, new(context.Action3)}} return s1Trans}
optional
optional與repeat類似^-^。
從語義層次來看,optional用來修飾Action或Procedure,說明該Action或Procedure最多執行一次,並且可以不執行。
從代碼層次來看,optional也是一個Fragment,因為其實現了該介面。
綜上,optional在本質上是對Action或Procedure的封裝,同時也有Fragment的行為。
optional的執行次數由謂詞Specification確定,Specification是一個interface,它的定義如下:
type Specification interface { Ok(s1Obj *s1obj.S1Obj, transInfo *context.TransInfo) bool}
謂詞的執行個體來自兩個方面的確認:
- 系統的某個開關是否開啟,即開關開啟時,謂詞為真,執行一次Action或Procedure,否則執行零次。
- 系統的目前狀態是否滿足某個條件,即條件滿足時,謂詞為真,執行一次Action或Procedure,否則執行零次。
有了optional後,我們可以定義一個事務如下:
func NewS1Trans() *Transaction { s1Trans := &Transaction{ fragments: []Fragment{ new(context.Action1), optional{spec:new(context.ShouldExecAction2), fragment:new(context.Action2)}, new(context.Action3), repeat{fragment:newA1Procedure()}, new(context.Action4)}} return s1Trans}
預設
從語義層次來看, 沒有repeat或optional修飾的Action或Procedure就是預設的情況,說明Action或Procedure僅且執行一次。
交易回復
對於事務來說,執行要麼成功,要麼失敗。當事務執行失敗時,必須觸發復原,使得系統無資源流失或殘留。
當事務執行失敗時,肯定實在某一個Fragment執行時失敗,我們記作fragments[i],交易回復的過程為:
- fragments[i]完成自己已指派的資源的回收和自己已寫入的資料的清理;
- 從fragments[i-1]到fragments[0],依次調用它的RollBack方法。
Action
Action是事務的原子執行者,從葉子節點來看,事務都是Action序列。
當某個Action執行失敗時,在Exec方法內進行該Action相關的資源回收或資料清理,不會調用該Action的RollBack函數。
Action的RollBack方法實現很簡單,僅進行該Action相關的所有資源回收和資料清理。
舉個例子:
Action5在執行失敗前,開啟了檔案file1,在表table1中寫了一條記錄,那麼它在返回error前要刪除表table1中的記錄,並關閉檔案file1,即逆序的進行資源回收和資料清理。
至於Action1到Action4中開啟了什麼資源或寫了什麼資料,Action5一點都不care。
Action5返回錯誤後,交易回復架構會自動依次調用[Action4,Action3, Action2, Action1]的Rollback函數,從而完成事務的復原。
Procedure
如果Procedure執行失敗,則在Exec方法中進行“錯誤處理”:
func (this *Procedure) Exec(knitterObj *knitterobj.KnitterObj, transInfo *context.TransInfo) error { index, err := for_each_fragments(this.fragments, knitterObj, transInfo) if err != nil { if index <= 0 { return err } back_each_fragments(this.fragments, knitterObj, transInfo, index) } return err}
Exec方法在實現中使用了事務層的原語for_each_fragments和back_each_fragments:
- 對於for_each_fragments原語,在事務過程式控制制一節中已經提過,即正向依次遍曆fragments,調用它的Exec方法。
- 對於back_each_fragments原語,先對入參index(最後一個參數)進行減一(index--),然後從index開始反向遍曆fragments,調用它的RollBack方法。
如果Procedure執行成功,復原時直接調用RollBack方法即可:
func (this *Procedure) RollBack(knitterObj *knitterobj.KnitterObj, transInfo *context.TransInfo) { back_each_fragments(this.fragments, knitterObj, transInfo, len(this.fragments))}
repeat
如果repeat執行失敗,則進行“錯誤處理”:
func (this repeat) Exec(knitterObj *knitterobj.KnitterObj, transInfo *context.TransInfo) error { for i := 0; i < transInfo.Times; i++ { transInfo.RepeatIdx = i err := this.fragment.Exec(knitterObj, transInfo) if err != nil { if i == 0 { return err } i-- for j := i; j >= 0; j-- { transInfo.RepeatIdx = j this.fragment.RollBack(knitterObj, transInfo) } return err } } return nil}
這裡的transInfo.RepeatIdx需要解釋一下:
- 在this.fragment.Exec之前賦值為i,是為了在repeat執行Action或Procedure時,找到對應的領域對象。
- this.fragment.RollBack之前賦值為j,是為了repeat在“錯誤處理”時,即復原已經完成的Action或Procedure時,找到對應的領域對象。舉個例子,比如repeat的最大次數是5,當進行到第4次時發生了錯誤,這時需要復原前三次的Action或Procedure。
如果repeat執行成功,復原時直接調用RollBack方法即可:
func (this repeat) RollBack(knitterObj *knitterobj.KnitterObj, transInfo *context.TransInfo) { for i := transInfo.Times; i >= 0; i-- { transInfo.RepeatIdx = i this.fragment.RollBack(knitterObj, transInfo) }}
optional
optional就比較簡單了,如果執行過程中發生了錯誤,則啥也不用幹,因為Action或Procedure已完成了錯誤處理,如下所示:
func (this optional) Exec(s1Obj *s1obj.S1Obj, transInfo *context.TransInfo) error { if this.spec.Ok(s1Obj, transInfo) { this.isExec = true return this.fragment.Exec(s1Obj, transInfo) } return nil}
如果optional執行成功,復原時需要根據是否執行過Action或Procedure來進行Action或Procedure的復原,如下所示:
func (this optional) RollBack(s1Obj *s1obj.S1Obj, transInfo *context.TransInfo) { if this.isExec { this.fragment.RollBack(s1Obj, transInfo) }}
事務並發
事務的執行過程是一個同步模型,而事務之間卻是非同步。多個事務間可能共用資源,所以要對事務進行並發控制。
在Golang中,協程之間的並發控制一般使用channel,非常簡單且高效。
假設一組協程使用一個共用資源,這時通過一個channel控制,那麼多組協程就需要多個channel來控制。我們可以使用map,key為shareId,value為channel。
讀channel
根據商務程序,要在某個Specification(謂詞,optional的第一個參數)中讀channel。假設該謂詞為IsSomethingNotExist,範例程式碼如下:
func (this *IsSomethingNotExist) Ok(s1Obj *s1obj.S1Obj, transInfo *TransInfo) bool { ... <- transInfo.Chan transInfo.ChanFlag = true ...}
要讀channel,必須先注入。根據局部化原則,我們在謂詞IsSomethingNotExist中進行注入,而不在前面的Action或Specification中進行注入,於是範例程式碼變為:
func (this *IsSomethingNotExist) Ok(s1Obj *s1obj.S1Obj, transInfo *TransInfo) bool { ... concurrencyctrl.ChanMapLock.Lock() value, ok := concurrencyctrl.ChanMap[shareId] if ok { transInfo.Chan = value } else { transInfo.Chan = make(chan int, 1) transInfo.Chan <- 1 concurrencyctrl.ChanMap[shareId] = transInfo.Chan } concurrencyctrl.ChanMapLock.Unlock() <- transInfo.Chan transInfo.ChanFlag = true ...}
寫channel
根據商務程序,要在讀channel的Specification之後的某個Action中寫channel。假設該Action為DiscussAction,範例程式碼如下:
func (this *DiscussAction) Exec(s1Obj *s1obj.S1Obj, transInfo *TransInfo) error { ... transInfo.Chan <- 1 transInfo.ChanFlag = false ...}
細心的讀者可能已經發現,上面的描述“要在讀channel的Specification之後的某個Action中寫channel”存在兩種情況:
- 該Specification是optional的第一個參數,而該Action或包含該Action的Procedure是對應的第二個參數
- 該Action在該Specification對應的optional操作之後
不管Specification的Ok方法是否返回true,第二種情況總是會進行寫channel操作,而第一種情況則未必,即當Specification的Ok方法返回為false時,並不會進行寫channel操作,所以有瑕疵。該瑕疵的修複方法是在該Specification的Ok方法內進行判斷,如果傳回值為false,則進行寫channel操作。假設該謂詞為IsSomethingNeedDel,則範例程式碼為:
func (this *IsSomethingNeedDel) Ok(s1Obj *s1obj.S1Obj, transInfo *TransInfo) bool { ... concurrencyctrl.ChanMapLock.Lock() value, ok := concurrencyctrl.ChanMap[shareId] if ok { transInfo.Chan = value } else { transInfo.Chan = make(chan int, 1) transInfo.Chan <- 1 concurrencyctrl.ChanMap[shareId] = transInfo.Chan } concurrencyctrl.ChanMapLock.Unlock() <- transInfo.Chan transInfo.ChanFlag = true ... if flag { log.Infof("***IsSomethingNeedDel: true***") } else { transInfo.Chan <- 1 transInfo.ChanFlag = false log.Infof("***IsSomethingNeedDel: false***") } return flag}
錯誤和異常處理
在事務執行過程中,不管是遇到錯誤還是發生了異常(panic),可能會出現對於channel讀了沒有寫的情況,即在交易處理過程中沒有實現channel的閉合操作,這將導致該組的其他協程(Goroutine)也阻塞了。
該問題的解決思路是在事務調度的入口方法中使用defer修飾的閉包對異常進行捕獲,同時針對錯誤或異常都對channel嘗試閉合操作,範例程式碼如下:
func scheduleS1ReqTrans(req []byte) (err error) { transInfo := &context.TransInfo{Names: make([]string, 0)} defer func() { if p := recover(); p != nil { str, ok := p.(string) if ok { err = errors.New(str) } else { err = errors.New("panic") } log.Info("S1ReqTrans panic recover start!") log.Error("Stack:", string(debug.Stack())) log.Info("S1ReqTrans panic recover end!") } if transInfo.ChanFlag { transInfo.Chan <- 1 } }() S1Obj := s1obj.CreateS1Obj(req) s1ReqTrans := trans.NewS1ReqTrans() err = s1ReqTrans.Exec(S1Obj, transInfo) if err != nil { s1ReqTrans.RollBack(S1Obj, transInfo) } return err}
小結
在管理域的組件中,對即時性和效能並沒有極致的要求,同時Goroutine非常輕量級,所以使用同步模型是一種非常聰明且簡單的處理方式。本文所討論的事務模型針對的就是同步過程,先詳細闡述了事務的過程式控制制,然後對事務的復原給出了通用的設計架構,最後對事務的並發控制給出了簡單高效的解決方案。事務模型在DDD的分層架構中位於第四層,代碼抽象層次高且表達力強,和商務程序圖一一對應,同時代碼可以以Action或Procedure為粒度進行複用。