在构建分布式系统时,开发者常常面临持久性和线程同步的复杂性问题。Orleans框架通过使用Actor模型(尽管它称之为"Grains")以及限制对它们的操作,使得操作变得线程安全,从而简化了这一过程。本文将介绍Orleans框架,并展示其如何帮助开发者构建容错、异步的分布式系统。
要运行本文中的代码以及使用Microsoft Orleans框架,需要.NET Framework v4.5(或更高版本)。此外,为了创建项目,使用了Orleans Tools for Visual Studio插件,以及Visual Studio 2015(专业版或更高版本,如果想使用插件)。Orleans Templates插件可以简化创建过程。
为了展示这项技术,构建了一个金融服务应用程序的大纲,该程序用于跟踪资金(共同基金、对冲基金等)。这是一个入门级应用程序,但认为足以展示这项技术所具有的潜力。
在简单系统中,将对以下实体进行建模:
第一步是创建一个新项目,定义grain接口(不同grain“实体”可以相互通信的方式)。对于每个实体,需要决定如何唯一地识别它。默认情况下,选项包括通过唯一字符串、整数或全局唯一ID(GUID)。在这个入门级演示中,将使用字符串,但如果实际实体没有内在的唯一标识符,那么可以使用GUID或增量整数。
接口定义了可以对grain(实体)实例执行的操作——例如,可能会说投资者可以订阅或从基金中赎回:
namespace FundGrainInterfaces
{
/// <summary>
/// Grain接口,用于投资者
/// </summary>
/// <remarks>
/// 对于这个示例,每个投资者都有一个唯一的字符串标识符来识别他们
/// </remarks>
public interface IInvestorGrain : IGrainWithStringKey
{
/// <summary>
/// 投资者订阅基金 - 支付资金
/// </summary>
/// <param name="fundToSubscribe">投资者订阅的基金</param>
/// <param name="amountToSubscribe">订阅的资金金额</param>
Task Subscribe(IFundGrain fundToSubscribe, decimal amountToSubscribe);
/// <summary>
/// 投资者从基金中提取资金
/// </summary>
/// <param name="fundToRedeem">投资者之前订阅的基金</param>
/// <param name="redemptionAmount">赎回的金额</param>
Task Redeem(IFundGrain fundToRedeem, decimal redemptionAmount);
}
}
然后,需要创建一个项目来在具体类中实现这些接口。每个具体的grain类必须继承自抽象类"Grain",并实现上述声明的接口。
为了说明grain之间的通信,当投资者订阅或从基金中赎回时,这些信息会传递给Fund grain:
/// <summary>
/// 投资者的Grain实现类。
/// </summary>
public class InvestorGrain : Grain, IInvestorGrain
{
// 投资者在基金中的持股
private Dictionary<string, decimal> _holdings = new Dictionary<string, decimal>();
public Task<decimal> Redeem(IFundGrain fundToRedeem, decimal redemptionAmount)
{
if (_holdings.ContainsKey(fundToRedeem.GetPrimaryKeyString()))
{
if (redemptionAmount <= _holdings[fundToRedeem.GetPrimaryKeyString()])
{
// 不能赎回没有持有的
redemptionAmount = _holdings[fundToRedeem.GetPrimaryKeyString()];
}
return fundToRedeem.Redemption(redemptionAmount);
}
else
{
// 没有持股,因此不能赎回
return Task.FromResult(decimal.Zero);
}
}
public Task Subscribe(IFundGrain fundToSubscribe, decimal amountToSubscribe)
{
fundToSubscribe.Subscription(amountToSubscribe);
if (_holdings.ContainsKey(fundToSubscribe.GetPrimaryKeyString()))
{
_holdings[fundToSubscribe.GetPrimaryKeyString()] += amountToSubscribe;
}
else
{
_holdings.Add(fundToSubscribe.GetPrimaryKeyString(), amountToSubscribe);
}
// 表示一切顺利
return TaskDone.Done;
}
}
相应的Fund grain可以处理来自基金自有可用现金的订阅和赎回:
public class FundGrain : Grain, IFundGrain
{
private decimal _liquidCash;
// 基金中可用的流动现金...
public Task<decimal> Redemption(decimal redemptionAmount)
{
if (_liquidCash >= redemptionAmount)
{
_liquidCash -= redemptionAmount;
}
else
{
// 赎回尽可能多的现金
redemptionAmount = _liquidCash;
_liquidCash = 0;
}
return Task.FromResult(redemptionAmount);
}
public Task Subscription(decimal subscriptionAmount)
{
_liquidCash += subscriptionAmount;
return TaskDone.Done;
}
}
Grain(实体)的实例需要由silo托管,silo实际上是grain的虚拟机环境。为了测试这些,需要创建一个silo测试项目:
在这个主机中,实例化grains(实体),然后可以通过它们定义的接口与它们进行交互:
// - - - 8< - - - - - - - - - - - - - - - - - - - - - - -
IFundGrain myTestFund = GrainClient.GrainFactory.GetGrain
现在,如果运行多个silo实例,它们将允许运行相同的底层fund grain,而开发者无需实现(或关心)任何并发检查。