Grains 是 Orleans 應(yīng)用程序的構(gòu)建塊,它們是彼此孤立的原子單位,分布的,持久的, 一個典型的 Grain 是有狀態(tài)和行為的一個單實(shí)例,每個 Grain 實(shí)例的在單線程內(nèi)執(zhí)行,Grain 之間共享數(shù)據(jù)通過消息傳遞,Grains 是由 Silo 自動化管理。
Grain 之間傳遞消息過程中也可能出現(xiàn)死鎖的情況,如:Grain A 發(fā)送消息給 Grain B,并等待它的完成,此時(shí) Grain B 發(fā)送一個消息給 Grain A,也等待其完成,這時(shí)候出現(xiàn)相互等待而造成死鎖。Orleans 對 Grain 之間產(chǎn)生的死鎖問題解決也是非常簡單的,只需要在 Grain上加 [Reentrant] 屬性,具體可查看官方 Concurrency。
Grain 狀態(tài)有好幾種存儲方式,比如:AzureTableStorage、AzureBlobStorage、SQLStorage、MemoryStorage 等,我們還可以自定義存儲。MemoryStorage 在測試項(xiàng)目使用沒問題,但實(shí)際生產(chǎn)環(huán)境要使用其他持久存儲的方式,因?yàn)橐坏┮粋€ Silo 被關(guān)閉,內(nèi)存存儲的狀態(tài)將會消失。
在分布式下,State 的使用可以減少很多對數(shù)據(jù)庫層面的壓力。當(dāng)然也不是所有的 Grain 都推薦使用 State,還是看實(shí)際業(yè)務(wù)需求。我們可以想象一個場景,一個商品的庫存如果保存在 State 中,所有請求都共享這個 State,在判斷是否有剩余商品的時(shí)候是不是就不需要每次都去查詢數(shù)據(jù)庫了?
定義接口
public interface IPersonGrain : IGrainWithStringKey
{
Task SayHelloAsync();
}
實(shí)現(xiàn)接口
public class PersonGrain : Grain, IPersonGrain
{
public Task SayHelloAsync()
{
string primaryKey = this.GetPrimaryKeyString();
Console.WriteLine($"{primaryKey} said hello!");
return Task.CompletedTask;
}
}
為了實(shí)現(xiàn)狀態(tài)存儲,我們需要創(chuàng)建一個 class:
public class PersonGrainState
{
public bool SaidHello { get; set; }
}
修改代碼,實(shí)現(xiàn)的 PersonGrain 不應(yīng)該再繼承 Grain,而是 Grain<T>
[StorageProvider(ProviderName = "OrleansStorage")]
public class PersonGrain : Grain<PersonGrainState>, IPersonGrain
{
public async Task SayHelloAsync()
{
string primaryKey = this.GetPrimaryKeyString();
bool saidHelloBefore = this.State.SaidHello;
string saidHelloBeforeStr = saidHelloBefore ? " already" : null;
Console.WriteLine($"{primaryKey}{saidHelloBeforeStr} said hello!");
this.State.SaidHello = true;
await this.WriteStateAsync();
}
}
第一次調(diào)用這個方法的時(shí)候 this.State.SaidHello 為 false,輸出 xxx said hello! 。然后我們通過 WriteStateAsync 修改 SaidHello 為 true,當(dāng)?shù)诙伪徽{(diào)用的時(shí)候,從 State 里取出的 SaidHello 已經(jīng)變成了 true,則輸出 xxx already said hello!
Orleans 提供了非常簡單的 API 來處理持久化裝狀態(tài),看方法名就知道什么意思了,WriteStateAsync()、ReadStateAsync() 、 ClearStateAsync()。
同時(shí)在 PersonGrain 加了一個 StorageProvider 屬性,參數(shù) ProviderName 賦值為 OrleansStorage,這里需要對 Silo 的配置文件(OrleansConfiguration.xml)做調(diào)整,添加 StorageProviders 配置,Type 表示存儲方式,Name 表示名稱,程序內(nèi)指定的 ProviderName 需要和配置中這個名稱保持一致。
注意:
當(dāng) Name為Default 時(shí),如果某個 Grain 使用 Default 來存儲,可以不需要加 StorageProvider 屬性。StorageProviders 下可以有多個 Provider,每個 Provider 的 Type 可以不一樣,每個 Grain 指定的存儲方式也可以不一樣,ProviderName 指定是誰就用誰存儲。
<?xml version="1.0" encoding="utf-8" ?>
<OrleansConfiguration xmlns="urn:orleans">
<Globals>
<SeedNode Address="localhost" Port="11111" />
<StorageProviders>
<Provider Type="Orleans.Storage.MemoryStorage"
Name="OrleansStorage" />
</StorageProviders>
</Globals>
<Defaults>
<Networking Address="localhost" Port="11111" />
<ProxyingGateway Address="localhost" Port="30000" />
</Defaults>
</OrleansConfiguration>
為了驗(yàn)證 Grain 之間是獨(dú)立的,在 Client 加入以下代碼:
var joe = GrainClient.GrainFactory.GetGrain<IPersonGrain>("Joe");
joe.SayHelloAsync();
joe.SayHelloAsync();
var sam = GrainClient.GrainFactory.GetGrain<IPersonGrain>("Sam");
sam.SayHelloAsync();
sam.SayHelloAsync();
測試結(jié)果:

SQL Server 持久存儲 State
上面提到 State 以內(nèi)存存儲的方式并不適合生產(chǎn)環(huán)境,那下面我們使用 SQL Server 來實(shí)現(xiàn)。
在 Silo 程序集中安裝依賴包:
Install-Package Microsoft.Orleans.OrleansSqlUtils
Install-Package System.Data.SqlClient
創(chuàng)建數(shù)據(jù)庫和表:
- 在 SQL Server 中創(chuàng)建一個數(shù)據(jù)庫,命名如:OrleansStorage(隨意);
- 在解決方案下找到目錄:packages\Microsoft.Orleans.OrleansSqlUtils.1.5.0\lib\net461\SQLServer,目錄下有一個 .sql 文件,在 OrleansStorage 數(shù)據(jù)庫下執(zhí)行這個 sql 腳本即可;
修改 OrleansConfiguration.xml 的 StorageProviders 節(jié)點(diǎn)為:
<StorageProviders>
<Provider Type="Orleans.Storage.AdoNetStorageProvider"
Name="OrleansStorage"
AdoInvariant="System.Data.SqlClient"
DataConnectionString="Server=.;Database=OrleansStorage;User ID=sa;Password=123456;"/>
</StorageProviders>
重新啟動 Silo 和 Client:
執(zhí)行完成后查看數(shù)據(jù)庫中表 Storage 的內(nèi)容,數(shù)據(jù)的值是二進(jìn)制是方式存儲。

之后不管重啟多少次,輸出的結(jié)果都是 xxx already saild hello! 。