程序在運行過程中有時會莫名其妙出現(xiàn)代碼的某些約束或者執(zhí)行結(jié)果和理想狀況不一樣,正常邏輯怎么會出現(xiàn)這樣的情況?到底發(fā)生了什么?好像見了鬼!瞬間好無助。
大多數(shù)出現(xiàn)正常邏輯很難解釋的時候,我們可能會想到并發(fā)問題,因為好像只有并發(fā)才會能說服自己。為了驗證和解決這個問題,我們可能會嘗試一些方案,在并發(fā)的情況下我相信很多人都使用過鎖,鎖確實也能幫忙我們解決問題,不然它干嘛存在。
但隨著業(yè)務邏輯的持續(xù)復雜,鎖的使用可能無處不在。首先大家都知道鎖本身的機制很耗性能;然后鎖本身不涉及什么編程模式,所以在業(yè)務代碼中融入大量鎖對代碼本身的穩(wěn)定性也有一定影響。
經(jīng)過查找資料,因為本身的項目是基于 .NET,所以發(fā)現(xiàn) Microsoft Orleans 好像可以比較好的滿足解決并發(fā)的需求。
Orleans 之前,先來扯一扯 Actor 模型
Actor 是以單線程存在的,所有消息都是順序到達的,每次收到消息后,就放入隊列,而它每次也從隊列中取出消息體來處理;
每一個 Actor 有一個 Id 和它對應,一個 Id 對應的 Actor 只會在 集群中 存在一個,使用者只需要通過 Id 就能隨時訪問不需要關注該 Actor 在集群的什么位置;
每一個 Actor 看作是一個獨立的實體,擁有自己獨立的狀態(tài)。Actor 與 Actor 之間可以進行消息通知;
注:有狀態(tài)的 Actor 在集群中一個 Id 只會存在一個實例,無狀態(tài)的可配置為根據(jù)流量存在多個,無狀態(tài)的情況看具體業(yè)務需求。
再來扯一扯 Orleans 框架
Orleans 提供了一個簡單的方法來構(gòu)建大規(guī)模、高并發(fā)、分布式應用程序,被認為是 Actor 模型的分布式版本,是一種改進的 Actor 模型。在 Orleans 中,Actors 被稱作 Grains,采用接口來表示,Actors 的消息用異步方法來接收,方法返回值必須是 Task or Task<T>。
Orleans幾個核心角色:
Grains(Actors)
Grains 是 Orleans 應用程序的業(yè)務邏輯實現(xiàn)與抽象,Grains 是彼此孤立的原子單位,分布的,持久的。 一個典型的 Grain 是有狀態(tài)和行為的一個單實例。
Silo
Silo 是一個主機服務,里面主要用于執(zhí)行 Grains,也就是說 Grains 開發(fā)完成后需要注冊到 Silo 中,然后等待調(diào)用。它監(jiān)聽一個端口,用來監(jiān)聽從 Silo 到 Silo 的消息或者從客戶端到 Silo 的消息的,典型的 Silo 就是,每臺機器運行一個 Silo,會對外暴露網(wǎng)關地址供調(diào)用。
Cluster(集群搭建的時候會具體介紹)
大量的 Silo 同時在一起工作就形成了 Orleans 的集群,Orleans 運行完全自動化的集群管理。
Client
具體的應用客戶端,可以是控制臺、Web 應用程序、WPF 等一切 .NET 端技術。
開始接觸 Orleans Sample 的時候,第一感覺項目結(jié)構(gòu)和 gRPC 還挺像的,如果你之前有接觸,一定感覺很親切:
- 定義一個接口(Interfaces)
- 實現(xiàn)接口(Grains) -- 添加引用Interfaces
- 啟動服務端(Silo)-- 添加引用Interfaces,Grains
- 啟動客戶端 (Client)-- 添加引用Interfaces
練習過程中對 NuGet 安裝 Orleans 相關依賴包可能會有一些模糊,這里說明一下我的具體步驟,希望盡快幫忙實現(xiàn)效果,所有程序集使用 .NET Framework 的版本都是 4.6:
程序集名稱 | 類型 | NuGet 依賴包 Microsoft.Orleans. |
引用 |
---|---|---|---|
Interfaces | 類庫 | Core | - |
Grains | 類庫 | Core | Interfaces |
Silo | 控制臺程序 | Core OrleansCodeGenerator OrleansProviders OrleansRuntime |
Interfaces Grains |
Client | 控制臺程序 | Core OrleansCodeGenerator |
Interfaces |
在 Silo 項目中添加配置文件 OrleansConfiguration.xml:
<?xml version="1.0" encoding="utf-8" ?>
<OrleansConfiguration xmlns="urn:orleans">
<Globals>
<SeedNode Address="localhost" Port="11111" />
</Globals>
<Defaults>
<Networking Address="localhost" Port="11111" />
<ProxyingGateway Address="localhost" Port="30000" />
</Defaults>
</OrleansConfiguration>
SeedNode:集群中主 Silo 地址,生產(chǎn)環(huán)境下不要這么使用。以這種方式配置主 Silo 的情況下,其他 Silo 加入集群需要等主 Silo 先啟動。之后會介紹 SystemStore 來維護集群成員關系;
Networking:內(nèi)部 Silo 與 Silo 之間通信地址;
ProxyingGateway:客戶端調(diào)用的網(wǎng)關地址;
在 Client 項目中添加配置文件 ClientConfiguration.xml:
<?xml version="1.0" encoding="utf-8" ?>
<ClientConfiguration xmlns="urn:orleans">
<Gateway Address="localhost" Port="30000"/>
</ClientConfiguration>
Gateway:配置 Silo 對外的網(wǎng)關地址;
集群下可以配置多個 Gateway 節(jié)點,如下:
<Gateway Address="gateway1" Port="30000"/>
<Gateway Address="gateway2" Port="30000"/>
注意:配置文件需要設置屬性 "復制到輸出目錄"
Grain 說明:
每個 Grain 都是單實例的,具有唯一標識。根據(jù)唯一標識獲取 Grain,這個標識可以是 GUID、String、Long、混合類型。
在 Grain 內(nèi)如果發(fā)送消息給其他 Grain,需要使用 this.GrainFactory.GetGrain,不能通過 GrainClient.GrainFactory.GetGrain。
var test = GrainClient.GrainFactory.GetGrain<ITest>(0); // long類型的primaryKey 0
public class TestGrain : Orleans.Grain, ITest
{
private int num = 0;
public Task AddCount()
{
num++;
Console.WriteLine(num);
return Task.CompletedTask;
}
}
Client 說明:
同時啟動3個 Task,每個 Task 內(nèi)并行200次調(diào)用 AddCount 方法。如果沒有做特殊的處理,num 的結(jié)果肯定是亂的,并不會出現(xiàn)一直累加的效果。
private static void DoClientWork()
{
var t1 = Task.Factory.StartNew(() =>
{
AddCount();
});
var t2 = Task.Factory.StartNew(() =>
{
AddCount();
});
var t3 = Task.Factory.StartNew(() =>
{
AddCount();
});
Task.WaitAll(t1, t2, t3);
}
static void AddCount()
{
var test = GrainClient.GrainFactory.GetGrain<ITest>(0);
Parallel.For(0, 200, (i) =>
{
test.AddCount();
});
}
實際上執(zhí)行最終的結(jié)果是600,并不會出現(xiàn)不一致的變化效果,這足以說明同一個 Grain 內(nèi)部是單線程執(zhí)行。