NATS: 使用 work-queue Stream
提醒
请注意对 Consumer 的使用,见 NATS: Pull Consumers in JetStream
基于 JsStream 使用队列
借助 JetStream,还可以将流用作队列,方法是将保留策略设置为 WorkQueuePolicy,并利用拉取使用者 轻松实现处理的水平可扩展性(或将显式 ack push 使用者与订阅者队列组一起使用)。
排队异地亲和性
当连接到全球分布的 NATS 超级集群时,存在自动服务异地亲和性,因为如果集群上没有可用于本地处理请求的侦听器,则服务请求消息只会路由到另一个集群(即另一个区域)。
原文地址:https://docs.nats.io/nats-concepts/core-nats/queue#stream-as-a-queue
代码示例
原文地址:https://natsbyexample.com/examples/jetstream/workqueue-stream/dotnet2
提醒,在 NATS 中,我们可以通过 Consumer 视图来访问 Stream 中的消息。Consumer 概念提供了对于消息的中间映射。
A work-queue retention policy satisfies a very common use case of queuing up messages that are intended to be processed once and only once.
Work-queue 的保持策略用于处理非常常见的场景,将消息排队,期望它被处理一次并且仅仅一次。
This retention policy supports queuing up messages from publishers independent of consummption. Since each message is intended to be processed only once, this retention type allows for a set of consumers that have non-overlapping interest on subjects.
In other words, if multiple consumers are bound to a work-queue stream, they must have disjoint filter subjects. This is in contrast to a standard limits-based or interest-based stream which supports multiple consumers with overlapping interest.
此保留策略支持对来自发布者的消息进行排队,这与消费消息的处理速度无关。由于每条消息只处理一次,因此此保留类型允许一组对主题具有非重叠兴趣
的 Consumer。
换言之,如果多个 Consumer 绑定到工作队列流,则它们必须具有不相交的筛选器主题。这与基于限制或基于兴趣的标准流相反,该流支持具有重叠兴趣的多个 Consumer。
注:如果有多个 Consumer 使用了相同的主题订阅,
- 在没有使用过滤的情况下,会得到异常
multiple non-filtered consumers not allowed on workqueue stream
- 使用了相同过滤的情况下,会得到异常
filtered consumer not unique on workqueue stream
Like the interest policy this retention policy is additive to any limits set on the stream. As a contrived example, if max-msgs is set to one with old messages being discarded, every new message that is received by the stream will result in the prior message being deleted regardless if any subscriptions were available to process the message.
与基于兴趣的策略一样,此保留策略是对流上设置的任何限制的附加。举个人为的例子,如果将 max-msgs 设置为丢弃旧消息的 max-msgs,则流收到的每条新消息都将导致删除前一条消息,而不管是否有任何订阅可用于处理该消息。
In this example, we will walk through the work-queue retention setup and behavior. If you are new to streams, it is recommended to read the limits-based stream example prior to reading this one.
在此示例中,我们将演练工作队列保留设置和行为。如果您不熟悉流,建议在阅读本文之前先阅读基于限制的流示例。
代码 Code
安装 NuGet 包 NATS.NET 和 Microsoft.Extensions.Logging.Console.
> dotnet add package NATS.Net
> dotnet add package NATS.Client.Serializers.Json
> dotnet add package Microsoft.Extensions.Logging.Console
示例代码
using Microsoft.Extensions.Logging;
using NATS.Client.Core;
using NATS.Client.JetStream;
using NATS.Client.JetStream.Models;
// 基于 Console 的日志器
using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
var logger = loggerFactory.CreateLogger("NATS-by-Example");
NATS_URL 环境变量用来提供 NATS 服务器的地址。
var url = Environment.GetEnvironmentVariable("NATS_URL") ?? "127.0.0.1:4222";
连接到 NATS 服务器。因为连接是 Disposable 的,在代码 Scope 的最后,我们应该刷新缓冲区并明确关闭连接。
var opts = new NatsOpts
{
Url = url,
LoggerFactory = loggerFactory,
Name = "NATS-by-Example",
};
await using var nats = new NatsConnection(opts);
创建 JScontext,访问 JetStream 用来管理 Stream 和 Consumer,还包括用于发布消息到流中和消费流中的消息。
var js = new NatsJSContext(nats);
var streamName = "EVENTS";
创建 Stream
定义流的配置,指定保持策略为 WorkQueuePolicy,并创建流。以后不会基于 NATS 连接操作,而是基于 JS 的 Stream 进行操作了。
var stream = await js.CreateStreamAsync(new StreamConfig(streamName, new[] { "events.>" })
{
Retention = StreamConfigRetention.Workqueue,
});
入队消息
就是普通的发布消息。
// 发布一些消息到流中。
await js.PublishAsync("events.us.page_loaded", "event-data");
await js.PublishAsync("events.us.mouse_clicked", "event-data");
await js.PublishAsync("events.us.input_focused", "event-data");
logger.LogInformation("published 3 messages");
// 检查流的信息,我们可以看到有 3 条消息已经入队了。
logger.LogInformation("# Stream info without any consumers");
await PrintStreamStateAsync(stream);
增加一个 Consumer
Consumer 相当于数据库中的视图。
现在我们增加一个 Consumer 来消费消息,并发布更多的消息。
var consumer = await stream.CreateConsumerAsync(new ConsumerConfig("processor-1"));
// 提取并响应入队的消息
await foreach (var msg in consumer.FetchAsync<string>(opts: new NatsJSFetchOpts { MaxMsgs = 3 }))
{
// 响应消息已经处理
await msg.AckAsync();
/* await msg.AckAsync(new AckOpts { DoubleAck = true }); */
}
// 再次检查 stream 的信息,注意到已经没有消息在队列中了。
logger.LogInformation("# Stream info with one consumer");
await PrintStreamStateAsync(stream);
独占非过滤的 Consumer
As noted in the description above, work-queue streams can only have at most one consumer with interest on a subject at any given time. Since the pull consumer above is not filtered, if we try to create another one, it will fail.
如前所述,对于 work-queue 流来说,在任何时间,最多只能一个 Consumer 对一个主题。由于上面的 Consumer 是非过滤的,如果我们试图创建同一主题的另外一个 Consumer ,新的将会失败。
logger.LogInformation("# Create an overlapping consumer");
try
{
await stream.CreateConsumerAsync(new ConsumerConfig("processor-2"));
}
catch (NatsJSApiException e)
{
logger.LogInformation("Error: {Message}", e.Error);
}
However if we delete the first one, we can then add the new one.
不过,如果我们删除了前面的那个 Consumer,然后我们可以创建一个新的。
await stream.DeleteConsumerAsync("processor-1");
await stream.CreateConsumerAsync(new ConsumerConfig("processor-2"));
logger.LogInformation("Created the new consumer");
await stream.DeleteConsumerAsync("processor-2");
多过滤的 Consumer
为了创建多个 Consumer,需要使用主题 subject 的过滤器。例如,我们需要限制每个 Consumer 到事件发布的地理位置上,在下面的示例中,是 us
或者 eu
注意,这里增加了对 FilterSubject 的配置。
var consumer1 = await stream.CreateConsumerAsync(new ConsumerConfig("processor-us") { FilterSubject = "events.us.>" });
var consumer2 = await stream.CreateConsumerAsync(new ConsumerConfig("processor-eu") { FilterSubject = "events.eu.>" });
await js.PublishAsync("events.eu.mouse_clicked", "event-data");
await js.PublishAsync("events.us.page_loaded", "event-data");
await js.PublishAsync("events.us.input_focused", "event-data");
await js.PublishAsync("events.eu.page_loaded", "event-data");
logger.LogInformation("Published 4 messages");
await foreach (var msg in consumer1.FetchAsync<string>(opts: new NatsJSFetchOpts { MaxMsgs = 2 }))
{
logger.LogInformation("us sub got: {Subject}", msg.Subject);
await msg.AckAsync();
}
await foreach (var msg in consumer2.FetchAsync<string>(opts: new NatsJSFetchOpts { MaxMsgs = 2 }))
{
logger.LogInformation("eu sub got: {Subject}", msg.Subject);
await msg.AckAsync();
}
logger.LogInformation("Bye!");
async Task PrintStreamStateAsync(INatsJSStream jsStream)
{
await jsStream.RefreshAsync();
var state = jsStream.Info.State;
logger.LogInformation(
"Stream has messages:{Messages} first:{FirstSeq} last:{LastSeq} consumer_count:{ConsumerCount} num_subjects:{NumSubjects}",
state.Messages,
state.FirstSeq,
state.LastSeq,
state.ConsumerCount,
state.NumSubjects);
}
NatsJSPubOpts
public record NatsJSPubOpts : NatsPubOpts
{
public static readonly NatsJSPubOpts Default = new();
// ttl time.Duration
// id string
public string? MsgId { get; init; }
// lid string // Expected last msgId
public string? ExpectedLastMsgId { get; init; }
// str string // Expected stream name
public string? ExpectedStream { get; init; }
// seq *uint64 // Expected last sequence
public ulong? ExpectedLastSequence { get; init; }
// lss *uint64 // Expected last sequence per subject
public ulong? ExpectedLastSubjectSequence { get; init; }
// Publish retries for NoResponders err.
// rwait time.Duration // Retry wait between attempts
public TimeSpan RetryWaitBetweenAttempts { get; init; } = TimeSpan.FromMilliseconds(250);
// rnum int // Retry attempts
public int RetryAttempts { get; init; } = 2;
}
posted @ 2025-03-14 16:04 冠军 阅读(4) 评论(0) 推荐(0) 编辑