NATS: Pull Consumers in JetStream
原文地址:https://natsbyexample.com/examples/jetstream/pull-consumer/csharp
对 Consumer 使用拉取模式,支持订阅到 Consumer 的应用程序根据需要来提取一个或者多个消息。这样应用程序可以自己来控制消息到达的流程,应用程序可以在自己控制的合适的时间范围来处理它。
Consumer 本身可以是持久的或者暂时的。持久的 Consumer 将在 NATS 服务器上跟踪自己的状态,特别重要的是,来自客户端的最新的确认消息。
对于一次性使用的场景来说,暂时的 Consumer 更加方便,对于资源使用和管理来说也更加轻量。不过,在订阅者取消订阅之后,暂时的 Consumer 不会持久化。NATS 服务器将会在一个时间间隔之后,自动清理 (删除) 该 Consumer.
因为每个订阅者根据需要来提取消息,多个订阅者可以创建绑定到相同的拉取 Consumer,而不需要任何其它的配置。每个订阅者都可以提取一批消息,然后并发的处理它们。
特别要提醒的是,在一批中的消息相对其它消息来说是有序的,但是,每个订阅者会独立的处理成批的消息,如果需要确定性分区以实现可扩展的订单处理,请在此处了解更多信息。
代码示例
1. 安装 NATS.Net 包
using System.Diagnostics;
using NATS.Client.JetStream;
using NATS.Client.JetStream.Models;
using NATS.Net;
2. 通过环境变量获得 NATS 连接串
这里尝试使用环境变量 NATS_URL 来获得 NATS 连接串。
var url = Environment.GetEnvironmentVariable("NATS_URL") ?? "nats://127.0.0.1:4222";
3. 连接到 NATS
连接到 NATS 服务器。因为连接在我们指定的 Scope 之外是 Disposable 可丢弃的,我们应该清理缓存并清楚关闭连接。
await using var nc = new NatsClient(url);
4. 获得 JetStream
底层是 JetStream, 通过 JetStream 来管理 Stream,使用基于 JetStream 的 Consumer 发布消息和消息消息。
var js = nc.CreateJetStreamContext();
5. 定义流
定义一个简单的基于限制的流。
var streamName = "EVENTS";
var stream = await js.CreateStreamAsync(new StreamConfig(streamName, subjects: ["events.>"]));
6. 发布一些消息
通过流发布消息
await js.PublishAsync(subject: "events.1", data: "event-data-1");
await js.PublishAsync(subject: "events.2", data: "event-data-2");
await js.PublishAsync(subject: "events.3", data: "event-data-3");
7. 创建基于前述创建的流的 Consumer
基于前面创建的 Stream 创建 Consumer。如果没有提供持久化的名称,当没有活动的消息的时候,Consumer 将在 InactiveThreshold (默认是 5 秒钟) 时间到达之后之后被删除掉。
Consumer 的名称是可选的,如果没有提供,Consumer 的名字将被自动生成。对于本示例来说,我们使用了没有提供的方式,这样它将是临时的,使用了自动生成的名称。
var consumer = await stream.CreateOrUpdateConsumerAsync(new ConsumerConfig());
8. 通过 Consumer 消费消息
使用 Consume() 方法,可以在循环中持续消息消息。Consume() 方法支持多种形式的选项,不过在本例中,我们使用默认的 ones.break 作为本例来确保在处理了 3 个消息之后停止处理 (这样它不会影响其它的示例)
var count = 0;
await foreach (var msg in consumer.ConsumeAsync<string>())
{
await msg.AckAsync();
Console.WriteLine($"received msg on {msg.Subject} with data {msg.Data}");
if (++count == 3)
break;
}
此时前面发布的 3 个消息应该被处理掉了。
9. 再发布一些消息
await js.PublishAsync(subject: "events.1", data: "event-data-1");
await js.PublishAsync(subject: "events.2", data: "event-data-2");
await js.PublishAsync(subject: "events.3", data: "event-data-3");
10. 批量提取消息
可以批量提取消息。第一个参数是批量的尺寸,它指定了最多可以提取的消息数量。对第一次提取,我们使用了 2 个消息,我们会得到 2 个消息,因为前面我们已经准备了 3 个消息。
var fetchCount = 0;
await foreach (var msg in consumer.FetchAsync<string>(opts: new NatsJSFetchOpts { MaxMsgs = 2 }))
{
await msg.AckAsync();
fetchCount++;
}
Console.WriteLine($"Got {fetchCount} messages");
11. 使用 FetchNoWait() 方法提取消息
通过返回的 Messages() 通道来获取发布到其中的消息。
该通道仅仅在请求数量的消息被接收到之后,或者超时之后才会被关闭。如果我们并不希望等待剩下的消息,并且希望在获得当前存在的消息之后快速返回 (基于提供的批的尺寸),我们应该使用 FetchNoWait() 方法来替代 Fetch() 方法。
所以,因为我们已经接收了 2 个消息,我们会仅仅得到 1 个剩下的消息。
注意:FetchNoWait() 方法并不鼓励使用,因为没有正确使用的话,它会导致额外的负担。例如,在没有规避空队列处理的循环中使用这个方法,会导致在 Stream 中没有消息的情况下,还持续尝试提取消息。
fetchCount = 0;
await foreach (var msg in ((NatsJSConsumer)consumer).FetchNoWaitAsync<string>(opts: new NatsJSFetchOpts { MaxMsgs = 100 }))
{
await msg.AckAsync();
fetchCount++;
}
Console.WriteLine($"Got {fetchCount} messages");
12. Fetch() 导致阻塞
最后,如果流已经为空,我们到达了流的末尾,并且调用了 Fetch() 方法,该调用将会被阻塞直到 "max wait" 时间,默认是 30s 秒钟,不过,这个时间可以作为选项显式配置。
var fetchStopwatch = Stopwatch.StartNew();
fetchCount = 0;
await foreach (var msg in consumer.FetchAsync<string>(opts: new NatsJSFetchOpts { MaxMsgs = 100, Expires = TimeSpan.FromSeconds(1) }))
{
await msg.AckAsync();
fetchCount++;
}
Console.WriteLine($"Got {fetchCount} messages in {fetchStopwatch.Elapsed}");
13. 持久化的 Consumer
持久化的 Consumer 需要通过提供持久化的名称来创建。持久化的 Consumer 不会因为 InactiveThreshold 时间被自动删除。它需要通过调用 DeleteConsumer() 来删除。
var durable = await stream.CreateOrUpdateConsumerAsync(new ConsumerConfig("processor"));
14. 通过持久化 Consumer 提取消息是不变的
从使用端来说,使用持久化的 Consumer 和暂时的 Consumer 没有区别。
await foreach (var msg in durable.FetchAsync<string>(opts: new NatsJSFetchOpts { MaxMsgs = 1 }))
{
Console.WriteLine($"Received {msg.Subject} from durable consumer");
}
15. 删除持久 Consumer
对于暂时的 Consumer 来时,在 InactiveThreshold 之后会自动删除,在不需要之后,持久的 Consumer 必须显式删除。
await stream.DeleteConsumerAsync("processor");
16. 确认不存在的持久化 Consumer
使用 try/catch 来确认持久化的 Consumer 已经被删除了。
try
{
await stream.GetConsumerAsync("processor");
}
catch (NatsJSApiException e)
{
if (e.Error.Code == 404)
{
Console.WriteLine("Consumer is gone");
}
}
17. 完成
就这样了。
Console.WriteLine("Bye!");
posted @ 2025-02-10 11:22 冠军 阅读(18) 评论(0) 推荐(0) 编辑