事件驅動模型相信對大家來說並不陌生,因爲這是一套非常高效的邏輯處理模型,通過事件來驅動接下來需要完成的工作,而不像傳統同步模型等待任務完成後再繼續!雖然事件驅動有着這樣的好處,但在傳統設計上基於消息回調的處理方式在業務處理中相對比較麻煩整體設計成本也比較高,所以落地也不容易。EventNext
是一個事件驅動的應用框架,它的事件驅動支持接口調用,在一系列的業務接口調用過程中通過事件驅動調用來完成;簡單來說組件驅動的接口行爲是由上一接口行爲完成而觸發執行,接下來介紹詳細介紹一下EventNext
和使用。
NextQueue
EventNext
組件有一個核心的事件驅動隊列NextQueue
,NextQueue
和傳統的線程隊列有着很大的區別;傳統隊列都是線程不停的執行消息,下一個消息都是線程等待上一個消息完成後再繼續。但NextQueue
的設計則不是,它的所有消息都基於上一個消息完成來驅動(不管上一個消息的邏輯是同步還是異步)。實際情況是NextQueue
觸發任務的消息是啓用線程工作外,後面的消息都是基於上一個消息回調執行;NextQueue
上的消息執行線程是不確定性也不需要等待,雖然隊列裏的消息執行線程不是唯一的,但執行順序是一致的這也是NextQueue
所帶來的好處,在有序的情況下確保線程的利用率更高。
組件使用
在使用組前需要引用組件,Nuget安裝如下
Install-Package EventNext
通過組件制定的業務必須以接口的方式來描述,而業務的調用也是通過接口的方式進行;雖然組件支持以消息回調的方式便不建議這樣做,畢竟面向業務接口有着更好的易用性和可維護性。爲了確保業務接口方式 的行爲滿足事件驅動隊列的要求 ,所有業務行爲方法必須以Task作爲返回值;非Task返回值的行爲方法都不能被組件註冊和調用。
接口定義和實現
接口的定義有一定的規則,除了方法返回值是Task
外,也不支持同一名稱的函數進行重載,如果有需要可以使用特定的Attribute
來標記對應的名稱(out類型參數不被支持)。以下是一個簡單的接口定義:
1 public interface IUserService 2 { 3 4 Task<int> Income(int value); 5 6 Task<int> Payout(int value); 7 8 Task<int> Amount(); 9 10 }
業務實現:
1 [Service(typeof(IUserService))] 2 public class UserService : IUserService 3 { 4 private int mAmount; 5 6 public Task<int> Amount() 7 { 8 return Task.FromResult(mAmount); 9 } 10 11 public Task<int> Income(int value) 12 { 13 mAmount += value; 14 return Task.FromResult(mAmount); 15 } 16 17 public Task<int> Payout(int value) 18 { 19 mAmount -= value; 20 return Task.FromResult(mAmount); 21 } 22 23 }
需要通過ServiceAttribute
來描這個類提供那些事件驅動的接口行爲。
使用
組件通過一個EventCenter
的對象來進行邏輯調用,創建該對象並註冊相應業務功能的程序集即可:
EventCenter eventCenter = new EventCenter(); eventCenter.Register(typeof(Program).Assembly);
定義EventCenter
加載邏輯後就可以創建代理接口調用
var service=EventCenter.Create<IUserService>(); await server.Payout(10); await server.Income(10);
事件驅動隊列分配
組件針對不同情況的需要,可以給接口實例或方法定義不同的事件隊列配置,主要爲以下幾種情況
默認
由組件內部隊列組進行負載情況進行配置,這種分配方式會導致同一接口的方法有可能分配在不同的隊列上;在默認分配下接口實例的方法會存在多線程中同時的運行,因此這種模式的應用並不是線程安全。
Actor
Actor
相信大家也很熟悉,一種高性能一致性的調度模型;組件支持這種模型的接口實例創建,只需要在創建接口代理的時候指定Actor
名稱即可
henry = EventCenter.Create<IUserService>("henry");
當指定Actor
名稱後,這個接口的所有方法調用都會一致性到對應實例的隊列中,即所有功能方法線程調用的唯一性;在接口調用返回的時候也會再次切入到其他事件驅動隊列,確保Actor
內部的工作隊列不受響後的應邏輯影響;當使用這種方式時整個Actor實例都是線程安全的。
ThreadPool
這種配置只適用於接口方法,描述方法無論什麼情況都從線程池中執行相關代碼,此行爲的方法非線程安全
1 [ThreadInvoke(ThreadType.ThreadPool)] 2 public Task<int> ThreadInvoke() 3 { 4 mCount++; 5 return mCount.ToTask(); 6 }
SingleQueue
這種配置只適用於接口方法,用於描述方法不管那個實例都一致性到一個隊列中,此行爲的方法內線程安全,不保證對應實例是線程安全.
1 [ThreadInvoke(ThreadType.SingleQueue)] 2 public Task<int> GetID([ThreadUniqueID]string name) 3 { 4 if (!mValues.TryGetValue(name, out int value)) 5 { 6 value = 1; 7 } 8 else 9 { 10 value++; 11 } 12 mValues[name] = value; 13 return value.ToTask(); 14 }
在這配置下還可以再細分,如上面的[ThreadUniqueID]
對不同參數做一致性對列,這個時候name的不同值會一致性到不同的事件隊列中。
Actor性能對比
組件默認集成了Actor
模型,可以通過它實現高併發無鎖業務集成,EventNext
最大的特點是以接口的方式集成應用,相對於akka.net
基於消息接收的模式來說有着明顯的應用優勢。在性能上EventNext
基於接口的ask機制也比akka.net
基於消息receive的ask機制要高,以下是一個簡單的對比測試
akak.net
1 public class UserActor : ReceiveActor 2 { 3 public UserActor() 4 { 5 Receive<Income>(Income => 6 { 7 mAmount += Income.Memory; 8 this.Sender.Tell(mAmount); 9 }); 10 Receive<Payout>(Outlay => 11 { 12 mAmount -= Outlay.Memory; 13 this.Sender.Tell(mAmount); 14 }); 15 Receive<Get>(Outlay => 16 { 17 this.Sender.Tell(mAmount); 18 }); 19 } 20 private decimal mAmount; 21 } 22 //invoke 23 Income income = new Income { Memory = i }; 24 var result = await nbActor.Ask<decimal>(income); 25 Payout payout = new Payout { Memory = i }; 26 var result = await nbActor.Ask<decimal>(payout);
Event Next
1 [Service(typeof(IUserService))] 2 public class UserService : IUserService 3 { 4 private int mAmount; 5 6 public Task<int> Amount() 7 { 8 return Task.FromResult(mAmount); 9 } 10 11 public Task<int> Income(int value) 12 { 13 mAmount += value; 14 return Task.FromResult(mAmount); 15 } 16 17 public Task<int> Payout(int value) 18 { 19 mAmount -= value; 20 return Task.FromResult(mAmount); 21 } 22 } 23 //invoke 24 var result = await nb.Income(i); 25 var result = await nb.Payout(i);
詳細測試代碼https://github.com/IKende/EventNext/tree/master/samples/EventNext_AkkaNet 在默認配置下不同併發下的測試結果
Event Sourcing
由於事件驅動提倡的業務處理都是異步,這樣就帶來一個業務事務性的問題,如何確保不同接口方法業務處理一致性就比較關鍵了。由於不同的邏輯在不同線程中異步進行,所以相對比較好解決的就是在業務處理時引入Event Sourcing
.以下就簡單介紹一下組件這方面的應用,就不詳細介紹了。畢竟 Event Sourcing設計和業務還有着一些關係
1 public async Task<long> Income(int amount) 2 { 3 await EventCenter.WriteEvent(this, null, null, new { History = user.Amount, Change = amount, Value = user.Amount + amount }); 4 user.Amount += amount; 5 return user.Amount; 6 } 7 8 public async Task<long> Pay(int amount) 9 { 10 await EventCenter.WriteEvent(this, null, null, new { History = user.Amount, Change = -amount, Value = user.Amount - amount }); 11 user.Amount -= amount; 12 return user.Amount; 13 }
組件提供事件信息的讀寫接口IEventLogHandler
可以通過實現這個接口擴展自己的事件源處理。
使用注意事項
適應async/await
其實整個事件隊列都是使用async/await
,通過它大大簡化了消息和回調函數間不同數據狀態整合的難度。.Net
也現有所異步API都支持async/wait
。
異步化設計你的邏輯
在實現接口邏輯的情況儘可能使和異步邏輯方法,在邏輯實施過程中禁用Task.Wait
或一些線程相關Wait
的方法,特別不帶超時的Wait
因爲這種操作極容易導致事件驅動隊列邏輯被掛起,導致隊列無法正常工作;更糟糕的情況可能引起事件隊列假死的情況。
傳統異步API
由於各種原因,可能還存在舊的異步API不支持async/wait
,出現這情況可以通過TaskCompletionSource
來擴展已經有的異步方法支持async/wait