Golang事務模型

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

序言

筆者在《軟體設計的演變過程》一文中,將通訊系統軟體的DDD分層模型最終演化為五層模型,即調度層(Schedule)、事務層(Transaction DSL)、環境層(Context)、領域層(Domain)和基礎設施層(Infrastructure),我們簡單回顧一下:


ddd-layer-with-dci-dsl.png
  1. 調度層:維護UE的狀態模型,只包括業務的本質狀態,將接收到的訊息派發給事務層。
  2. 事務層:對應一個商務程序,比如UE Attach,將各個同步訊息或非同步訊息的處理組合成一個事務,當事務失敗時,進行復原。當事務層收到調度層的訊息後,委託環境層的Action進行處理。
  3. 環境層:以Action為單位,處理一條同步訊息或非同步訊息,將Domain層的領域對象cast成合適的role,讓role互動起來完成商務邏輯。
  4. 領域層:不僅包括領域對象及其之間關係的建模,還包括對象的角色role的顯式建模。
  5. 基礎實施層:為其他層提供通用的技術能力,比如訊息通訊機制、對象持久化機制和通用的演算法等

本文將聚焦於事務層,主要討論事務模型,代碼抽象層次和商務程序圖一一對應。

同步模型

毫無疑問,非同步模型是複雜的。但在管理域的組件中,對即時性和效能並沒有極致的要求,同時協程(比如,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}

謂詞的執行個體來自兩個方面的確認:

  1. 系統的某個開關是否開啟,即開關開啟時,謂詞為真,執行一次Action或Procedure,否則執行零次。
  2. 系統的目前狀態是否滿足某個條件,即條件滿足時,謂詞為真,執行一次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],交易回復的過程為:

  1. fragments[i]完成自己已指派的資源的回收和自己已寫入的資料的清理;
  2. 從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:

  1. 對於for_each_fragments原語,在事務過程式控制制一節中已經提過,即正向依次遍曆fragments,調用它的Exec方法。
  2. 對於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需要解釋一下:

  1. 在this.fragment.Exec之前賦值為i,是為了在repeat執行Action或Procedure時,找到對應的領域對象。
  2. 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”存在兩種情況:

  1. 該Specification是optional的第一個參數,而該Action或包含該Action的Procedure是對應的第二個參數
  2. 該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為粒度進行複用。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.