使用EventNext實現基於事件驅動的業務處理

smark發表於2019-05-24

事件驅動模型相信對大家來說並不陌生,因為這是一套非常高效的邏輯處理模型,通過事件來驅動接下來需要完成的工作,而不像傳統同步模型等待任務完成後再繼續!雖然事件驅動有著這樣的好處,但在傳統設計上基於訊息回撥的處理方式在業務處理中相對比較麻煩整體設計成本也比較高,所以落地也不容易。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

專案地址

https://github.com/IKende/EventNext

相關文章