fix: 批量队列消费者串行处理,避免 channel 并发导致消息长期 Unacked
AsyncEventingBasicConsumer + Prefetch>1 时 IModel 非线程安全,并发 Ack 易卡死;改为同队列串行消费并加固 Ack/Nack。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2d229bbedd
commit
7bc7cdf417
@ -17,6 +17,4 @@ public sealed class OrderBatchCreateConsumer(
|
|||||||
: OrderBatchQueueConsumerBase(channelPool, options, scopeFactory, logger)
|
: OrderBatchQueueConsumerBase(channelPool, options, scopeFactory, logger)
|
||||||
{
|
{
|
||||||
protected override string QueueName => OrderBatchQueueTopology.QueueCreate;
|
protected override string QueueName => OrderBatchQueueTopology.QueueCreate;
|
||||||
|
|
||||||
protected override bool UseRetryLimit => true;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,4 @@ public sealed class OrderBatchIntegrationConsumer(
|
|||||||
: OrderBatchQueueConsumerBase(channelPool, options, scopeFactory, logger)
|
: OrderBatchQueueConsumerBase(channelPool, options, scopeFactory, logger)
|
||||||
{
|
{
|
||||||
protected override string QueueName => OrderBatchQueueTopology.QueueIntegration;
|
protected override string QueueName => OrderBatchQueueTopology.QueueIntegration;
|
||||||
|
|
||||||
protected override bool UseRetryLimit => true;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,11 @@ public abstract class OrderBatchQueueConsumerBase : BackgroundService
|
|||||||
private readonly OrderBatchRabbitMqOptions _options;
|
private readonly OrderBatchRabbitMqOptions _options;
|
||||||
private readonly IServiceScopeFactory _scopeFactory;
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
/// <summary>
|
||||||
|
/// RabbitMQ.Client 的 IModel 非线程安全;AsyncEventingBasicConsumer + Prefetch>1
|
||||||
|
/// 会并发进 Received,若同时 Ack/业务访问同一 channel,容易卡死并导致消息长期 Unacked。
|
||||||
|
/// </summary>
|
||||||
|
private readonly SemaphoreSlim _processGate = new(1, 1);
|
||||||
private IChannelAccessor? _channelAccessor;
|
private IChannelAccessor? _channelAccessor;
|
||||||
private IModel? _channel;
|
private IModel? _channel;
|
||||||
|
|
||||||
@ -33,11 +38,6 @@ public abstract class OrderBatchQueueConsumerBase : BackgroundService
|
|||||||
|
|
||||||
protected abstract string QueueName { get; }
|
protected abstract string QueueName { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 为 true 时,Reject+Requeue 受 <see cref="OrderBatchRabbitMqOptions.MaxRetryCount"/> 限制。
|
|
||||||
/// </summary>
|
|
||||||
protected virtual bool UseRetryLimit => false;
|
|
||||||
|
|
||||||
protected override Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
if (!_options.Enabled)
|
if (!_options.Enabled)
|
||||||
@ -50,14 +50,19 @@ public abstract class OrderBatchQueueConsumerBase : BackgroundService
|
|||||||
|
|
||||||
_channelAccessor = _channelPool.Acquire(QueueName);
|
_channelAccessor = _channelPool.Acquire(QueueName);
|
||||||
_channel = _channelAccessor.Channel;
|
_channel = _channelAccessor.Channel;
|
||||||
_channel.BasicQos(0, _options.PrefetchCount, false);
|
// Prefetch 仅表示「最多同时 Unacked 多少条」;实际处理仍串行,避免 channel 并发。
|
||||||
|
var prefetch = _options.PrefetchCount == 0 ? (ushort)1 : _options.PrefetchCount;
|
||||||
|
_channel.BasicQos(0, prefetch, false);
|
||||||
|
|
||||||
var consumer = new AsyncEventingBasicConsumer(_channel);
|
var consumer = new AsyncEventingBasicConsumer(_channel);
|
||||||
consumer.Received += OnReceivedAsync;
|
consumer.Received += OnReceivedAsync;
|
||||||
|
|
||||||
_channel.BasicConsume(queue: QueueName, autoAck: false, consumer: consumer);
|
_channel.BasicConsume(queue: QueueName, autoAck: false, consumer: consumer);
|
||||||
|
|
||||||
_logger.LogInformation("Order batch consumer started on {Queue}.", QueueName);
|
_logger.LogInformation(
|
||||||
|
"Order batch consumer started on {Queue} (prefetch={Prefetch}, serial processing).",
|
||||||
|
QueueName,
|
||||||
|
prefetch);
|
||||||
|
|
||||||
stoppingToken.Register(() =>
|
stoppingToken.Register(() =>
|
||||||
{
|
{
|
||||||
@ -81,47 +86,60 @@ public abstract class OrderBatchQueueConsumerBase : BackgroundService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _processGate.WaitAsync().ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ProcessDeliveredAsync(ea).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_processGate.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessDeliveredAsync(BasicDeliverEventArgs ea)
|
||||||
|
{
|
||||||
|
if (_channel == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var message = OrderBatchJobProcessor.Deserialize(ea.Body);
|
var message = OrderBatchJobProcessor.Deserialize(ea.Body);
|
||||||
if (message == null)
|
if (message == null)
|
||||||
{
|
{
|
||||||
_channel.BasicAck(ea.DeliveryTag, false);
|
SafeAck(ea.DeliveryTag);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Consuming {Queue} action={Action} jobId={JobId} itemId={JobItemId} deliveryTag={DeliveryTag}",
|
||||||
|
QueueName,
|
||||||
|
message.Action,
|
||||||
|
message.JobId,
|
||||||
|
message.JobItemId,
|
||||||
|
ea.DeliveryTag);
|
||||||
|
|
||||||
using var scope = _scopeFactory.CreateScope();
|
using var scope = _scopeFactory.CreateScope();
|
||||||
var processor = scope.ServiceProvider.GetRequiredService<OrderBatchJobProcessor>();
|
var processor = scope.ServiceProvider.GetRequiredService<OrderBatchJobProcessor>();
|
||||||
var result = await processor.ProcessAsync(message);
|
var result = await processor.ProcessAsync(message).ConfigureAwait(false);
|
||||||
|
|
||||||
switch (result.Kind)
|
switch (result.Kind)
|
||||||
{
|
{
|
||||||
case OrderBatchJobProcessor.ProcessResultKind.Ack:
|
case OrderBatchJobProcessor.ProcessResultKind.Ack:
|
||||||
_channel.BasicAck(ea.DeliveryTag, false);
|
SafeAck(ea.DeliveryTag);
|
||||||
break;
|
break;
|
||||||
case OrderBatchJobProcessor.ProcessResultKind.Reject:
|
case OrderBatchJobProcessor.ProcessResultKind.Reject:
|
||||||
if (result.Requeue)
|
if (result.Requeue)
|
||||||
{
|
{
|
||||||
if (UseRetryLimit)
|
// 瞬时失败重投;次数耗尽时 Processor 会返回 Ack(DB 已记 Failed)
|
||||||
{
|
SafeNack(ea.DeliveryTag, requeue: true);
|
||||||
if (message.RetryCount < _options.MaxRetryCount)
|
|
||||||
{
|
|
||||||
message.RetryCount++;
|
|
||||||
_channel.BasicNack(ea.DeliveryTag, false, true);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_channel.BasicAck(ea.DeliveryTag, false);
|
// 业务失败:Ack 丢掉消息,状态已在 DB 记 Failed
|
||||||
}
|
SafeAck(ea.DeliveryTag);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_channel.BasicNack(ea.DeliveryTag, false, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_channel.BasicAck(ea.DeliveryTag, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@ -130,12 +148,42 @@ public abstract class OrderBatchQueueConsumerBase : BackgroundService
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Order batch consumer failed on queue {Queue}.", QueueName);
|
_logger.LogError(ex, "Order batch consumer failed on queue {Queue}.", QueueName);
|
||||||
_channel.BasicNack(ea.DeliveryTag, false, true);
|
SafeNack(ea.DeliveryTag, requeue: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SafeAck(ulong deliveryTag)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_channel?.BasicAck(deliveryTag, false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "BasicAck failed on {Queue}, deliveryTag={DeliveryTag}", QueueName, deliveryTag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SafeNack(ulong deliveryTag, bool requeue)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_channel?.BasicNack(deliveryTag, false, requeue);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
ex,
|
||||||
|
"BasicNack failed on {Queue}, deliveryTag={DeliveryTag}, requeue={Requeue}",
|
||||||
|
QueueName,
|
||||||
|
deliveryTag,
|
||||||
|
requeue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Dispose()
|
public override void Dispose()
|
||||||
{
|
{
|
||||||
|
_processGate.Dispose();
|
||||||
_channelAccessor?.Dispose();
|
_channelAccessor?.Dispose();
|
||||||
base.Dispose();
|
base.Dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user