Grains 是 Orleans 應用程序的構建塊,它們是彼此孤立的原子單位,分布的,持久的, 一個典型的 Grain 是有狀態和行為的一個單實例,每個 Grain 實例的在單線程內執行,Grain 之間共享數據通過消息傳遞,Grains 是由 Silo 自動化管理。
Grain 之間傳遞消息過程中也可能出現死鎖的情況,如:Grain A 發送消息給 Grain B,并等待它的完成,此時 Grain B 發送一個消息給 Grain A,也等待其完成,這時候出現相互等待而造成死鎖。Orleans 對 Grain 之間產生的死鎖問題解決也是非常簡單的,只需要在 Grain上加 [Reentrant] 屬性,具體可查看官方 Concurrency。
Grain 狀態有好幾種存儲方式,比如:AzureTableStorage、AzureBlobStorage、SQLStorage、MemoryStorage 等,我們還可以自定義存儲。MemoryStorage 在測試項目使用沒問題,但實際生產環境要使用其他持久存儲的方式,因為一旦一個 Silo 被關閉,內存存儲的狀態將會消失。
在分布式下,State 的使用可以減少很多對數據庫層面的壓力。當然也不是所有的 Grain 都推薦使用 State,還是看實際業務需求。我們可以想象一個場景,一個商品的庫存如果保存在 State 中,所有請求都共享這個 State,在判斷是否有剩余商品的時候是不是就不需要每次都去查詢數據庫了?
定義接口
public interface IPersonGrain : IGrainWithStringKey
{
Task SayHelloAsync();
}
實現接口
public class PersonGrain : Grain, IPersonGrain
{
public Task SayHelloAsync()
{
string primaryKey = this.GetPrimaryKeyString();
Console.WriteLine($"{primaryKey} said hello!");
return Task.CompletedTask;
}
}
為了實現狀態存儲,我們需要創建一個 class:
public class PersonGrainState
{
public bool SaidHello { get; set; }
}
修改代碼,實現的 PersonGrain 不應該再繼承 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();
}
}
第一次調用這個方法的時候 this.State.SaidHello 為 false,輸出 xxx said hello!
。然后我們通過 WriteStateAsync 修改 SaidHello 為 true,當第二次被調用的時候,從 State 里取出的 SaidHello 已經變成了 true,則輸出 xxx already said hello!
Orleans 提供了非常簡單的 API 來處理持久化裝狀態,看方法名就知道什么意思了,WriteStateAsync()、ReadStateAsync() 、 ClearStateAsync()。
同時在 PersonGrain 加了一個 StorageProvider 屬性,參數 ProviderName 賦值為 OrleansStorage,這里需要對 Silo 的配置文件(OrleansConfiguration.xml)做調整,添加 StorageProviders 配置,Type 表示存儲方式,Name 表示名稱,程序內指定的 ProviderName 需要和配置中這個名稱保持一致。
注意:
當 Name為Default 時,如果某個 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>
為了驗證 Grain 之間是獨立的,在 Client 加入以下代碼:
var joe = GrainClient.GrainFactory.GetGrain<IPersonGrain>("Joe");
joe.SayHelloAsync();
joe.SayHelloAsync();
var sam = GrainClient.GrainFactory.GetGrain<IPersonGrain>("Sam");
sam.SayHelloAsync();
sam.SayHelloAsync();
測試結果:
SQL Server 持久存儲 State
上面提到 State 以內存存儲的方式并不適合生產環境,那下面我們使用 SQL Server 來實現。
在 Silo 程序集中安裝依賴包:
Install-Package Microsoft.Orleans.OrleansSqlUtils
Install-Package System.Data.SqlClient
創建數據庫和表:
- 在 SQL Server 中創建一個數據庫,命名如:OrleansStorage(隨意);
- 在解決方案下找到目錄:packages\Microsoft.Orleans.OrleansSqlUtils.1.5.0\lib\net461\SQLServer,目錄下有一個 .sql 文件,在 OrleansStorage 數據庫下執行這個 sql 腳本即可;
修改 OrleansConfiguration.xml 的 StorageProviders 節點為:
<StorageProviders>
<Provider Type="Orleans.Storage.AdoNetStorageProvider"
Name="OrleansStorage"
AdoInvariant="System.Data.SqlClient"
DataConnectionString="Server=.;Database=OrleansStorage;User ID=sa;Password=123456;"/>
</StorageProviders>
重新啟動 Silo 和 Client:
執行完成后查看數據庫中表 Storage 的內容,數據的值是二進制是方式存儲。
之后不管重啟多少次,輸出的結果都是 xxx already saild hello!
。