Orleans 解决并发之痛(四):Streams

Orleans 提供了 Stream 扩展编程模型。此模型提供了一套 API,使处理流更简单和更健壮。Stream 默认提供了两种 Provider,Simple Message Stream ProviderAzure Queue Stream Provider,不同的流类型可能使用不同的 Provider 来处理。Stream Providers 兼容现有的队列技术,比如: Event HubsServiceBusAzure QueuesApache Kafka,不需要编写额外的代码来配合这些队列技术的使用。

关于为什么Orleans会提供Stream扩展编程模型?

当今已经有一系列技术可以来构建一个流处理系统。包括持久存储流数据方面,如:Event HubsKafka;数据流计算操作方面,如: Azure Stream AnalyticsApache StormApache Spark Streaming, 而这些技术并不适合细粒度的自由格式的流数据计算, 或者支持的并不好,因为实际情况下可能需要对不同的数据流执行不同的操作,Orleans Streams目的就是解决这类问题,Stream 编程模型和发布订阅模式挺相似。

上述提到的一些技术我并没有详细学习,后面会了解并对比。

Orleans Stream 大概实现的步骤如下:

  1. 获取 StreamProvider
  2. 获取 IAsyncStream<T>
  3. 订阅者订阅一个Stream
  4. 发布者向某个Stream发布消息

Silo 配置文件 OrleansConfiguration.xml 修改

在 Globals 节点中添加:

<StorageProviders>
  <Provider Type="Orleans.Storage.MemoryStorage" Name="PubSubStore" />
</StorageProviders>
<StreamProviders>
  <Provider Type="Orleans.Providers.Streams.SimpleMessageStream.SimpleMessageStreamProvider" Name="SMSProvider"/>
</StreamProviders>

Name 为 PubSubStore(名字任意) 的 StorageProvider 是必须的,Stream 内部需要它来跟踪所有流订阅,记录各个流的发布者和订阅者的关系,本例测试使用 MemoryStorage,实际生产环境不能使用 Memory 方式。

Name 为 SMSProvider(名字任意)的 StreamProvider 指定了消息的发布形式,Orleans 当前提供的两种 StreamProvider:Simple Message Stream ProviderAzure Queue Stream Provider 都是可靠的。

Simple Message Stream Provider:不保证可靠的交付,失败的消息不会自动重新发送,但可以根据返回的Task状态来判断是否重新发送,事件执行顺序遵循 FIFO 原则。

Azure Queue Stream Provider:事件被加入 Azure Queue, 如果传送或处理失败,事件不会从队列中删除,并且稍后会自动重新被发送,因此事件执行顺序不遵循 FIFO 原则。

获取 StreamProvider

var streamProvider = this.GetStreamProvider("SMSProvider");

SMSProvider 对应配置文件中 Name 为 SMSProvider 的 StreamProvider

获取 IAsyncStream<T>

var streamId = this.GetPrimaryKey();
var stream = streamProvider.GetStream<string>(streamId, "GrainStream");

GetStream 需要两个参数,通过两个值定位唯一的 Stream:
streamId:guid 类型,stream 标识
streamNamespace:字符串,stream 的命名空间

订阅一个Stream

订阅 Stream 分为隐式和显式订阅。

隐式订阅

隐式订阅模式订阅者自动由发布者创建,订阅者是唯一的,不存在对一个 Stream 的多次订阅,也不可手动取消订阅。

Interface:

public interface IImplicitSubscriberGrain : IGrainWithGuidKey
{
}

Grain:

[ImplicitStreamSubscription("GrainImplicitStream")]
public class ImplicitSubscriberGrain : Grain, IImplicitSubscriberGrain, IAsyncObserver<string>
{
  protected StreamSubscriptionHandle<string> streamHandle;

  public override async Task OnActivateAsync()
  {
    var streamId = this.GetPrimaryKey();
    var streamProvider = this.GetStreamProvider("SMSProvider");
    var stream = streamProvider.GetStream<string>(streamId, "GrainImplicitStream");
    streamHandle = await stream.SubscribeAsync(OnNextAsync);
  }

  public override async Task OnDeactivateAsync()
  {
    if (streamHandle != null)
      await streamHandle.UnsubscribeAsync();
  }

  public Task OnCompletedAsync()
  {
    return Task.CompletedTask;
  }

  public Task OnErrorAsync(Exception ex)
  {
    return Task.CompletedTask;
  }

  public Task OnNextAsync(string item, StreamSequenceToken token = null)
  {
    Console.WriteLine($"Received message:{item}");
    return Task.CompletedTask;
  }
}
  1. 在 Grain 上标记 ImplicitStreamSubscription 属性,变量值为命名空间;
  2. 在 Grain 的 OnActivateAsync 方法体中调用 SubscribeAsync ,Grain 创建即订阅;
  3. 实现 IAsyncObserver 接口,当发布者向 Stream 发送消息,订阅者接到消息后将执行 OnNextAsync;

显式订阅

Interface:

public interface IExplicitSubscriberGrain : IGrainWithGuidKey
{
  Task<StreamSubscriptionHandle<string>> SubscribeAsync();

  Task ReceivedMessageAsync(string data);
}

Grain:

public class ExplicitSubscriberGrain : Grain, IExplicitSubscriberGrain
{
  private IAsyncStream<string> stream;

  public async override Task OnActivateAsync()
  {
    var streamProvider = this.GetStreamProvider("SMSProvider");
    stream = streamProvider.GetStream<string>(this.GetPrimaryKey(), "GrainExplicitStream");
    var subscriptionHandles = await stream.GetAllSubscriptionHandles();
    if (subscriptionHandles.Count > 0)
    {
      subscriptionHandles.ToList().ForEach(async x =>
      {
        await x.ResumeAsync((payload, token) => this.ReceivedMessageAsync(payload));
      });
    }
  }

  public async Task<StreamSubscriptionHandle<string>> SubscribeAsync()
  {
    return await stream.SubscribeAsync((payload, token) => this.ReceivedMessageAsync(payload));
  }

  public Task ReceivedMessageAsync(string data)
  {
    Console.WriteLine($"Received message:{data}");
    return Task.CompletedTask;
  }
}
  1. 订阅者通过调用 SubscribeAsync 方法完成订阅,并返回 StreamSubscriptionHandle ,这个对象提供了 UnsubscribeAsync 方法,支持手动取消订阅;

  2. 本例子中支持对同一个 Stream 订阅多次,被订阅多次的结果是当向这个 Stream 发送消息的时候,ReceivedMessageAsync 会执行多次。如果不希望对同一个 Stream 订阅多次,在 SubscribeAsync 方法中可以通过GetAllSubscriptionHandles 获取当前已订阅者的数量,然后进行限制;

  3. 订阅者是一直存在的,除了被显示调用了 UnsubscribeAsync 方法。在 OnActivateAsync 中我们加入了 ResumeAsync 操作, 当 Grain 由未激活状态变为激活状态的时候,先通过 GetAllSubscriptionHandles 获取这个 Stream 中存在的订阅者,然后通过 ResumeAsync 可以把它们重新唤醒。(模拟方式:杀掉Silo,重新启动即可,不过前提条件是 PubSubStore 不能使用 MemoryStorage ,因为使用 MemoryStorage 存储一旦重启后订阅者和发布者的关系都会丢失)

发布消息

Interface:

public interface IPublisherGrain: IGrainWithGuidKey
{
  Task PublishMessageAsync(string data);
}

Grain:

public class PublisherGrain : Grain, IPublisherGrain
{
  private IAsyncStream<string> stream;

  public override Task OnActivateAsync()
  {
    var streamId = this.GetPrimaryKey();
    var streamProvider = this.GetStreamProvider("SMSProvider");
    this.stream = streamProvider.GetStream<string>(streamId, "GrainExplicitStream"); //隐式:GrainImplicitStream
    return base.OnActivateAsync();
  }

  public async Task PublishMessageAsync(string data)
  {
    Console.WriteLine($"Sending data: {data}");
    await this.stream.OnNextAsync(data);
  }
}

通过调用 IAsyncStream 的 OnNextAsync 发布消息即可。这里可以针对返回的 Task 状态再作一些操作,如果不成功,重新发送或记录日志等。

Client 发布消息:

客户端发布消息:

while (true)
{
  Console.WriteLine("Press 'exit' to exit...");
  var input = Console.ReadLine();
  if (input == "exit") break;
  var publisherGrain = GrainClient.GrainFactory.GetGrain<IPublisherGrain>(Guid.Empty);
  publisherGrain.PublishMessageAsync(input);
}
发布消息

显示订阅下,需要增加另一个客户端先完成订阅,可启动多个来创建多个订阅者:

var subscriberGrain = GrainClient.GrainFactory.GetGrain<IExplicitSubscriberGrain>(Guid.Empty);
var streamHandle = subscriberGrain.SubscribeAsync().Result;
Console.WriteLine("Press enter to exit...");
Console.ReadLine();
streamHandle.UnsubscribeAsync();
显示订阅下发布消息

参考链接:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • Grains 是 Orleans 应用程序的构建块,它们是彼此孤立的原子单位,分布的,持久的, 一个典型的 Gra...
    BeckJin阅读 4,590评论 5 11
  • 程序在运行过程中有时会莫名其妙出现代码的某些约束或者执行结果和理想状况不一样,正常逻辑怎么会出现这样的情况?到底发...
    BeckJin阅读 8,257评论 1 12
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,448评论 25 707
  • dict = {key:value,key:value} 通常标志:大括号{},键和值之间有冒号: 可以有空字典 ...
    HHusHH阅读 375评论 0 0