Orleans之EventSourcing
這是Orleans系列文章中的一篇.首篇文章在此
引入:
如果沒有意外,我再這篇文章中用ES代替EventSourcing,如果碰到"事件回溯","事件溯源","事溯"等詞語,都一般代表Eventsourcing.
如果引入Orleans而不用es的話,那就只用了Orleans一半的優點,多線程編程的邏輯\排錯的簡化以及可分布式.下面我聊聊重頭戲ES,這些都是我個人理解,如果有錯誤歡迎指正.
有時候產生一個理論是為了解決目前的困難,但是隨着事物的發展,原先的困難不再是主要困難,新的夜王必然會出現,聽聞三眼烏鴉要成為新的夜王我很是不解.馬丁這么寫會收到憤怒的讀者寄來的刀片,我估計馬丁把收到全世界的刀片都賣了,都比他的小說值錢.扯遠了。總而言之,我們關注的矛盾點會隨着時間變化而變化。以前為了節省存儲空間(當然並不僅僅是為了空間考慮),對數據庫的設計提出了三個范式,來指導數據庫的設計.可是現在時代變了狗蛋,存儲空間不值錢了.現在數據庫讀寫成為了新的瓶頸,所以我們設計數據庫的理念就應該變了.
ES遵循這一個簡單的思想,就是存儲的時候只存儲變化量,而不存儲最終結果.需要最終結果的地方,就必須提取所有的變化量以及初始狀態,讓它們相加得到最終結果.這樣的看起來是個麻煩的迂回(我不就是想得到最終結果嘛),但是卻隱藏着一個事實:由於只存儲變化量,意味着數據只增不減,意味着數據存儲后就不會被更改,意味着高並發和高吞吐量.但是有個缺點就是數據量增加很多,現在硬盤是最不值錢的,如果隨着時間的推移,變化量變的很多,要得到最終結果需要大量的運算,但是.這些缺點不算什么大缺點,有方法可以避免.雖然一直說空間不值錢,並不是說數據大小無關緊要,在合理的設計中,數據應當能小則小.
簡單的一句總結就是滿足只增不減,存后不改的數據,都能夠設計成ES.以便於提高吞吐量.
為啥只增不減會提供吞吐量?因為數據庫的數據永遠不變,所以就放心大膽的讀取吧。哪怕是多線程都不需要架鎖。可是這里隱藏着另一個風險,你讀取的數據不一定是最新的。這個"非最新"的確是個難題,不過好消息是,如果你一直讀,最終能夠讀到最新的。這就是"最終一致性"。
關於es網絡上有很多的文章,可以拿來讀讀,這里就不做過多的敘述了。
Orleans內置了三種ES的實現方式,但並不是我提到的ES.它內置的三種ES實現方式,分別是:StateStorage.LogConsistencyProvider,LogStorage.LogConsistencyProvider和CustomStorage.LogConsistencyProvider。這里我只使用介紹官方例子,它使用的是CustomStorage這個類。
使用Orleans達成ES,要明白幾個基本的概念,所謂的Event是個啥東西?在Orleans中Event一般是指的自定義的grain事件,這些事件的發生更改了grain的狀態。籠統的講就是更改了grain類相關的各種變量的值。那么你要把這些值存起來,就要問兩個問題,存到哪里去,用哪個類來控制存。又要要求溯源,那就必須讀,那就要解決用什么序列化的問題。這本質上是一個問題:用什么類控制存儲。
我打算拿官方的例子來介紹一下ES在orleans中基本實現方式。看完介紹,如果讀者要實現自己的ES,可以仿照更改。Orleans源碼里有一個例子是ReplicatedEventSample(以下簡稱RES),這個是我下面要說的重點。
RES例子是一個網頁例子,它的網頁如何體現的,以及如何運行RES,我將不做介紹,這里我只是重點說明網頁后邊的支撐程序:就是以下三個項目
其中主要設計的類總體圖像如下:
EventGrain是這個項目的主角,它接受到外界發送來的一些消息,把這些消息使用ES的方式存儲下來。在使用RES的時候,要注意EventGrain的基類以及需要實現的接口。
EventState是Grain的狀態值。
ReplicatedEventTable控制着實際的讀取與存儲。
GeneratorGrain一個消息制造器,它發送OutCome消息給EventGrain
TickerGrain,可有可無的東西。
它們工作流程如下
剛開始啟動的時候,silo會調用GeneratorGrain,這個GeneratorGrain的n個實例會激活EventGrain並每2.5秒發送一次OutCome消息。如下圖
EventGrain接受到激活指令后初始化一個ReplicatedEventTable實例用來控制自己的存儲,並調用RefreshNow()獲取最新的狀態,這時候orleans會調用ReadStateFromStorage,EventGrain在這里使用ReplicatedEventTable完成真正的讀取動作。
GeneratorGrain每隔2.5秒制造一個OutCome消息發送給EventGrain,EventGrain接受到消息並立即確認此消息,這時候可以在OnStateChanged函數中針對Grain狀態值變化做一些必要的處理。這些必要的處理不應該包括ES存儲動作,ES存儲動作會在ApplyUpdatesToStorage中進行處理,它里面使用了ReplicatedEventTable來進行真正的存儲。
這樣一個簡單的ES就實現了。這個ES里面的每一條數據都是OutCome,我們只需要改寫ReplicatedEventTable實現自己的存儲控制類。在本例子中,使用的是AzureTable作為存儲媒介的。
這里有幾個問題,如果我改寫后的新存儲控制類,假設名字是EventSql是一個普通類,在EventSql中我精確的控制了sql連接,sql讀取等等。因為EventGrain是一個key一個實例的。在一個soli中有可能存在非常多個Grain實例,這時候我再使用EventSql就有可能會產生很多的鏈接,導致出錯。要改進這里,只需要讓EventSql本身也是擴展自Grain(而不僅僅是一個普通的類)。同時使用一些機制來控制鏈接數。可以池化這些EventSqlGrain,使用的時候從池子中取出一個。
這樣官方的例子就算介紹完畢了,剩下的來聊聊一些其他細節的問題。
Orleans運行時會時不時的自動更新Grain的狀態值。但是有時候也是需要針對某些事件,我們自定義的去更新狀態值,要實現這樣的動作,可以重寫TransitionState 函數。
ReadStateFromStorage返回的值中需要有version 和對應的State。剛開始的時候,壓根什么都沒有存,那么就應該返回0和一個State的默認值。ApplyUpdatesToStorage有時候會存儲失敗,這時候運行時會進行重試。有時候雖然狀態值已經實際存儲了,但是卻返回一個錯誤,這樣會造成同一狀態值的重復存儲,這里就需要程序有一個過濾機制或者確保一個重復的狀態值不會對程序邏輯造成損害。
使用Orleans.EventSourcing.CustomStorage.LogConsistencyProvider(本例就是),它並不支持JournaledGrain類中的RetrieveConfirmedEvents方法,如果需要使用它,要另行實現。
這里只是我個人寫一些基礎的東西,具體改寫還是需要自己去實現,剛開始可以仿照這個RES進行改寫,把OutCome存儲到自己想要的地方。等改寫完畢就會一個大致的概念。
至此我寫完了orleans所有的主要方面,剩余的orleans底層的實現細節,還是需要自己對照文檔啃源碼了。
ES是實現業務需求的方法,一個項目本身是一個客觀物體,我們選擇了使用ES的辦法去實現它,會將這個項目的復雜度進行轉移,降低了業務在程序實現上的復雜度而同時增加了數據設計的復雜度。ES這個新的方法會讓某些復雜的業務邏輯變的簡單,特別適合於那些需要高並發與高吞吐量的場景。
開篇曾經說過的,"如果隨着時間的推移,變化量變的很多,要得到最終結果需要大量的運算"是個缺點,要克服這個缺點,可以這樣搞:每隔一段時間(比如一周)就計算所有的最終結果,並把這個最終結果當作新的初始值,同時把變化量轉移備份。簡單的如下圖所示: