- 文件說明
- 導讀
- 快速開始
- 訊息釋出者
- IMessagePublisher
- 連線池
- 訊息過期
- 事務
- 傳送方確認模式
- 獨佔模式
- 消費者
- 消費者模式
- 事件模式
- 分組
- 消費者模式
- 消費、重試和補償
- 消費失敗
- 自動建立佇列
- Qos
- 延遲佇列
- 空消費者
- 分組
- 事件匯流排模式
- 中介軟體
- 分組消費
- 配置
- 消費者配置
- 環境隔離
- 雪花 id 配置
- Qos 併發和順序
- Qos 場景
- 併發和異常處理
- 重試
- 重試時間
- 重試機制
- 持久化剩餘重試次數
- 死信佇列
- 死信佇列
- 延遲佇列
- 可觀測性
文件說明
作者:痴者工良
文件地址:https://mmq.whuanle.cn
倉庫地址:https://github.com/whuanle/Maomi.MQ
作者部落格:
-
https://www.whuanle.cn
-
https://www.cnblogs.com/whuanle
導讀
Maomi.MQ 是一個訊息通訊模型專案,目前只支援了 RabbitMQ。
Maomi.MQ.RabbitMQ 是一個用於專為 RabbitMQ 設計的釋出者和消費者通訊模型,大大簡化了釋出和訊息的程式碼,並提供一系列簡便和實用的功能,開發者可以透過框架提供的消費模型實現高效能消費、事件編排,框架還支援釋出者確認機制、自定義重試機制、補償機制、死信佇列、延遲佇列、連線通道複用等一系列的便利功能。開發者可以把更多的精力放到業務邏輯中,透過 Maomi.MQ.RabbitMQ 框架簡化跨程序訊息通訊模式,使得跨程序訊息傳遞更加簡單和可靠。
此外,框架透過 runtime 內建的 api 支援了分散式可觀測性,可以透過進一步使用 OpenTelemetry 等框架進一步收集可觀測性資訊,推送到基礎設施平臺中。
快速開始
本文將快速介紹 Maomi.MQ.RabbitMQ 的使用方法。
引入 Maomi.MQ.RabbitMQ 包,在 Web 配置中注入服務:
builder.Services.AddSwaggerGen();
builder.Services.AddLogging();
builder.Services.AddMaomiMQ((MqOptionsBuilder options) =>
{
options.WorkId = 1;
options.AppName = "myapp";
options.Rabbit = (ConnectionFactory options) =>
{
options.HostName = "192.168.3.248";
options.ClientProvidedName = Assembly.GetExecutingAssembly().GetName().Name;
};
}, [typeof(Program).Assembly]);
var app = builder.Build();
- WorkId: 指定用於生成分散式雪花 id 的節點 id,預設為 0。
- AppName:用於標識訊息的生產者,以及在日誌和鏈路追蹤中標識訊息的生產者或消費者。
- Rabbit:RabbitMQ 客戶端配置,請參考 ConnectionFactory。
如果是控制檯專案,則需要引入 Microsoft.Extensions.Hosting 包。
var host = new HostBuilder()
.ConfigureLogging(options =>
{
options.AddConsole();
options.AddDebug();
})
.ConfigureServices(services =>
{
services.AddMaomiMQ(options =>
{
options.WorkId = 1;
options.AppName = "myapp";
options.Rabbit = (ConnectionFactory options) =>
{
options.HostName = "192.168.3.248";
options.ClientProvidedName = Assembly.GetExecutingAssembly().GetName().Name;
};
}, new System.Reflection.Assembly[] { typeof(Program).Assembly });
// Your services.
services.AddHostedService<MyPublishAsync>();
}).Build();
await host.RunAsync();
定義訊息模型類,該模型類將會被序列化為二進位制內容傳遞到 RabbitMQ 伺服器中。
public class TestEvent
{
public int Id { get; set; }
public override string ToString()
{
return Id.ToString();
}
}
定義消費者,消費者需要實現 IConsumer<TEvent>
介面,以及使用 [Consumer]
特性註解配置消費者屬性。
[Consumer("test", Qos = 1, RetryFaildRequeue = true)]
public class MyConsumer : IConsumer<TestEvent>
{
private static int _retryCount = 0;
// 消費
public async Task ExecuteAsync(EventBody<TestEvent> message)
{
Console.WriteLine($"事件 id: {message.Id} {DateTime.Now}");
await Task.CompletedTask;
}
// 每次消費失敗時執行
public Task FaildAsync(Exception ex, int retryCount, EventBody<TestEvent>? message) => Task.CompletedTask;
// 補償
public Task<bool> FallbackAsync(EventBody<TestEvent>? message) => Task.FromResult(true);
}
然後注入 IMessagePublisher 服務釋出訊息:
[ApiController]
[Route("[controller]")]
public class IndexController : ControllerBase
{
private readonly IMessagePublisher _messagePublisher;
public IndexController(IMessagePublisher messagePublisher)
{
_messagePublisher = messagePublisher;
}
[HttpGet("publish")]
public async Task<string> Publisher()
{
// 釋出訊息
await _messagePublisher.PublishAsync(queue: "test", message: new TestEvent
{
Id = i
});
return "ok";
}
}
訊息釋出者
訊息釋出者用於推送訊息到 RabbitMQ 伺服器中。
透過注入 IMessagePublisher 介面即可向 RabbitMQ 推送訊息,示例專案請參考 PublisherWeb。
定義一個事件模型類:
public class TestEvent
{
public int Id { get; set; }
public override string ToString()
{
return Id.ToString();
}
}
注入 IMessagePublisher 服務後釋出訊息:
[ApiController]
[Route("[controller]")]
public class IndexController : ControllerBase
{
private readonly IMessagePublisher _messagePublisher;
public IndexController(IMessagePublisher messagePublisher)
{
_messagePublisher = messagePublisher;
}
[HttpGet("publish")]
public async Task<string> Publisher()
{
for (var i = 0; i < 100; i++)
{
await _messagePublisher.PublishAsync(queue: "PublisherWeb", message: new TestEvent
{
Id = i
});
}
return "ok";
}
}
IMessagePublisher
IMessagePublisher 定義比較簡單,只有三個方法和一個屬性:
public ConnectionPool ConnectionPool { get; }
Task PublishAsync<TEvent>(string queue, TEvent message, Action<IBasicProperties>? properties = null)
where TEvent : class;
Task PublishAsync<TEvent>(string queue, TEvent message, IBasicProperties properties);
// 不建議直接使用該介面。
Task CustomPublishAsync<TEvent>(string queue, EventBody<TEvent> message, BasicProperties properties);
三個 PublishAsync 方法用於釋出事件,ConnectionPool 屬性用於獲取 RabbitMQ.Client.IConnection 物件。
由於直接公開了 BasicProperties ,因此開發者完全自由配置 RabbitMQ 原生的訊息屬性,所以 Maomi.MQ.RabbitMQ 沒必要過度設計,只提供了簡單的功能介面。
例如,可以透過 BasicProperties 配置單條訊息的過期時間:
await _messagePublisher.PublishAsync(queue: "RetryWeb", message: new TestEvent
{
Id = i
}, (BasicProperties p) =>
{
p.Expiration = "1000";
});
當釋出一條訊息時,實際上框架傳遞的是 EventBody<T>
型別,EventBody<T>
中包含了一些重要的附加訊息屬性,這些屬性會給訊息處理和故障診斷帶來很大的方便。
public class EventBody<TEvent>
{
// 事件唯一 id.
public long Id { get; init; }
// Queue.
public string Queue { get; init; } = null!;
// App name.
public string Publisher { get; init; } = null!;
// 事件建立時間.
public DateTimeOffset CreationTime { get; init; }
// 事件體.
public TEvent Body { get; init; } = default!;
}
Maomi.MQ 透過 DefaultMessagePublisher 型別實現了 IMessagePublisher,DefaultMessagePublisher 預設生命週期是 Singleton:
services.AddSingleton<IMessagePublisher, DefaultMessagePublisher>();
生命週期不重要,如果需要修改預設的生命週期,可以手動修改替換。
services.AddScoped<IMessagePublisher, DefaultMessagePublisher>();
開發者也可以自行實現 IMessagePublisher 介面,具體示例請參考 DefaultMessagePublisher 型別。
連線池
為了複用 RabbitMQ.Client.IConnection ,Maomi.MQ.RabbitMQ 內部實現了 ConnectionPool 型別,透過物件池維護複用的 RabbitMQ.Client.IConnection 物件。
預設物件池中的 RabbitMQ.Client.IConnection 數量為 0,只有當連線被真正使用時才會從物件池委託中建立,連線物件會隨著程式併發量而自動增加,但是,預設最大連線物件數量為 Environment.ProcessorCount * 2
。
除了 IMessagePublisher 介面提供的 PublishAsync 方法可以釋出事件,開發者還可以從 ConnectionPool 獲取連線物件,請務必在使用完畢後透過 ConnectionPool.Return()
方法將其歸還到連線物件池。
透過連線池直接使用 IConnection 物件釋出訊息:
[HttpGet("publish")]
public async Task<string> Publisher()
{
for (var i = 0; i < 100; i++)
{
var connectionPool = _messagePublisher.ConnectionPool;
var connection = connectionPool.Get();
try
{
connection.Channel.BasicPublishAsync(
exchange: string.Empty,
routingKey: "queue",
basicProperties: properties,
body: _jsonSerializer.Serializer(message),
mandatory: true);
}
finally
{
connectionPool.Return(connection);
}
}
return "ok";
}
你也可以繞開 IMessagePublisher ,直接注入 ConnectionPool 使用 RabbitMQ 連線物件,但是不建議這樣使用。
private readonly ConnectionPool _connectionPool;
public DefaultMessagePublisher(ConnectionPool connectionPool)
{
_connectionPool = connectionPool;
}
public async Task MyPublshAsync()
{
var connection = _connectionPool.Get();
try
{
await connection.Channel.BasicPublishAsync(...);
}
finally
{
_connectionPool.Return(connection);
}
}
為了更加簡便地管理連線物件,可以使用 CreateAutoReturn()
函式建立連線管理物件,該物件被釋放時會自動將 IConnection 返還給連線池。
using var poolObject = _messagePublisher.ConnectionPool.CreateAutoReturn();
poolObject.Channel.BasicPublishAsync(
exchange: string.Empty,
routingKey: "queue",
basicProperties: properties,
body: _jsonSerializer.Serializer(message),
mandatory: true);
如果你自行使用 ConnectionPool 推送訊息到 RabbitMQ,請務必透過序列化 EventBody<TEvent>
事件物件,這樣 Maomi.MQ.RabbitMQ 消費者才能正常工作。同時,Moami.MQ 對可觀測性做了支援,如果自行使用 ConnectionPool 獲取連線物件推送訊息,可能會導致可觀測性資訊缺失。
正常情況下,RabbitMQ.Client 中包含了可觀測性的功能,但是 Maomi.MQ.RabbitMQ 附加的可觀測性資訊有助於診斷故障問題。
請注意:
-
Maomi.MQ.RqbbitMQ 透過
EventBody<TEvent>
泛型物件釋出和接收事件。 -
DefaultMessagePublisher 包含了鏈路追蹤等可觀測性程式碼。
訊息過期
IMessagePublisher 對外開放了 BasicProperties 或 BasicProperties,可以自由配置訊息屬性。
例如為訊息配置過期時間:
[HttpGet("publish")]
public async Task<string> Publisher()
{
for (var i = 0; i < 1; i++)
{
await _messagePublisher.PublishAsync(queue: "test", message: new TestEvent
{
Id = i
}, properties =>
{
properties.Expiration = "6000";
});
}
return "ok";
}
如果此時為 test
繫結死信佇列,那麼該訊息長時間沒有被消費時,會被移動到另一個佇列,請參考 死信佇列。
還可以透過配置訊息屬性實現更多的功能,請參考 IBasicProperties。
事務
RabbitMQ 支援事務,不過據 RabbitMQ 官方文件顯示,事務會使吞吐量減少 250 倍。
RabbitMQ 事務使用上比較簡單,可以保證釋出的訊息已經被推送到 RabbitMQ 伺服器,只有當提交事務時,提交的訊息才會被 RabbitMQ 儲存並推送給消費者。
使用示例:
[HttpGet("publish_tran")]
public async Task<string> Publisher_Tran()
{
using var tranPublisher = await _messagePublisher.TxSelectAsync();
try
{
await tranPublisher.PublishAsync(queue: "publish_tran", message: new TestEvent
{
Id = 666
});
await tranPublisher.TxCommitAsync();
}
catch
{
await tranPublisher.TxRollbackAsync();
throw;
}
return "ok";
}
或者手動開啟事務:
[HttpGet("publish_tran")]
public async Task<string> Publisher_Tran()
{
using var tranPublisher = _messagePublisher.CreateTransaction();
try
{
await tranPublisher.TxSelectAsync();
await tranPublisher.PublishAsync(queue: "publish_tran", message: new TestEvent
{
Id = 666
});
await tranPublisher.TxCommitAsync();
}
catch
{
await tranPublisher.TxRollbackAsync();
throw;
}
return "ok";
}
注意,在該種模式之下,建立 TransactionPublisher 物件時,會從物件池中取出一個連線物件,因為開啟事務模式可能會汙染當前連線通道,因此 TransactionPublisher 不會向連線池歸還連線物件,而是直接釋放。
傳送方確認模式
雖然事務模式可以保證訊息會被推送到 RabbitMQ 伺服器中,但是由於事務模式會導致吞吐量降低 250 倍,因此不是一個好的選擇。為了解決這個問題, RabbitMQ 引入了一種確認機制,這種機制就像滑動視窗,能夠保證訊息推送到伺服器中,並且具備高效能的特性。
請參考 https://www.rabbitmq.com/docs/confirms
使用示例:
[HttpGet("publish_confirm")]
public async Task<string> Publisher_Confirm()
{
using var confirmPublisher = await _messagePublisher.ConfirmSelectAsync();
for (var i = 0; i < 5; i++)
{
await confirmPublisher.PublishAsync(queue: "publish_confirm1", message: new TestEvent
{
Id = 666
});
var result = await confirmPublisher.WaitForConfirmsAsync();
// 如果在超時內沒有接收到 nacks,則為 True,否則為 false。
Console.WriteLine($"釋出 {i},{result}");
}
return "ok";
}
WaitForConfirmsAsync
方法會返回一個值,如果正常被伺服器確認了訊息已經傳達,則結果為 true,如果超時沒有被伺服器確認,則返回 false。
此外,還有一個 WaitForConfirmsOrDieAsync
方法,它會一直等待該頻道上的所有已釋出訊息都得到確認,使用示例:
using var confirmPublisher = await _messagePublisher.ConfirmSelectAsync();
for (var i = 0; i < 5; i++)
{
await confirmPublisher.PublishAsync(queue: "publish_confirm1", message: new TestEvent
{
Id = 666
});
Console.WriteLine($"釋出 {i}");
}
await confirmPublisher.WaitForConfirmsOrDieAsync();
注意,在該種模式之下,建立 ConfirmPublisher 物件時,會從物件池中取出一個連線物件,因為開啟事務模式可能會汙染當前連線通道,因此 ConfirmPublisher 不會向連線池歸還連線物件,而是直接釋放。
注意,同一個通道不能同時使用事務和傳送方確認模式。
獨佔模式
預設情況下,每次使用 IMessagePublisher.PublishAsync()
釋出訊息時,都會從連線池中取出連線物件,然後使用該連線通道釋出訊息,釋出完畢後就會歸還連線物件給連線池。
如果需要在短時間內大批次釋出訊息,則需要每次都要重複獲取和返還連線物件。
使用獨佔模式時可以在一段時間內獨佔一個連線物件,超出作用域後,連線物件會自動放回連線池。這種模式對於需要大量釋出訊息的場景提高吞吐量非常有幫助。為了能夠將連線通道歸還連線池,請務必使用 using
關鍵字修飾變數,或者手動呼叫 Dispose
函式。
使用示例:
// 建立獨佔模式
using var singlePublisher = _messagePublisher.CreateSingle();
for (var i = 0; i < 500; i++)
{
await singlePublisher.PublishAsync(queue: "publish_single", message: new TestEvent
{
Id = 666
});
}
消費者
Maomi.MQ.RabbitMQ 中,有兩種消費模式,一種是消費者模式,一種是事件模式(事件匯流排模式)。
下面簡單瞭解這兩種模式的使用方法。
消費者模式
消費者服務需要實現 IConsumer<TEvent>
介面,並且配置 [Consumer("queue")]
特性繫結佇列名稱,透過消費者物件來控制消費行為。
消費者模式有具有失敗通知和補償能力,使用上也比較簡單。
public class TestEvent
{
public int Id { get; set; }
}
[Consumer("PublisherWeb", Qos = 1, RetryFaildRequeue = true)]
public class MyConsumer : IConsumer<TestEvent>
{
private static int _retryCount = 0;
// 消費或重試
public async Task ExecuteAsync(EventBody<TestEvent> message)
{
_retryCount++;
Console.WriteLine($"執行次數:{_retryCount} 事件 id: {message.Id} {DateTime.Now}");
await Task.CompletedTask;
}
// 失敗
public Task FaildAsync(Exception ex, int retryCount, EventBody<TestEvent>? message) => Task.CompletedTask;
// 補償
public Task<bool> FallbackAsync(EventBody<TestEvent>? message) => Task.FromResult(true);
}
事件模式
事件模式是透過事件匯流排的方式實現的,以事件模型為中心,透過事件來控制消費行為。
[EventTopic("web2", Qos = 1, RetryFaildRequeue = true)]
public class TestEvent
{
public string Message { get; set; }
}
然後使用 [EventOrder]
特性編排事件執行順序。
// 編排事件消費順序
[EventOrder(0)]
public class My1EventEventHandler : IEventHandler<TestEvent>
{
public async Task CancelAsync(EventBody<TestEvent> @event, CancellationToken cancellationToken)
{
}
public async Task ExecuteAsync(EventBody<TestEvent> @event, CancellationToken cancellationToken)
{
Console.WriteLine($"{@event.Id},事件 1 已被執行");
}
}
[EventOrder(1)]
public class My2EventEventHandler : IEventHandler<TestEvent>
{
public async Task CancelAsync(EventBody<TestEvent> @event, CancellationToken cancellationToken)
{
}
public async Task ExecuteAsync(EventBody<TestEvent> @event, CancellationToken cancellationToken)
{
Console.WriteLine($"{@event.Id},事件 2 已被執行");
}
}
當然,事件模式也可以透過建立中介軟體增加補償功能,透過中介軟體還可以將所有排序事件放到同一個事務中,一起成功或失敗,避免事件執行時出現程式退出導致的一致性問題。
public class TestEventMiddleware : IEventMiddleware<TestEvent>
{
private readonly BloggingContext _bloggingContext;
public TestEventMiddleware(BloggingContext bloggingContext)
{
_bloggingContext = bloggingContext;
}
public async Task ExecuteAsync(EventBody<TestEvent> @event, EventHandlerDelegate<TestEvent> next)
{
using (var transaction = _bloggingContext.Database.BeginTransaction())
{
await next(@event, CancellationToken.None);
await transaction.CommitAsync();
}
}
public Task FaildAsync(Exception ex, int retryCount, EventBody<TestEvent>? message)
{
return Task.CompletedTask;
}
public Task<bool> FallbackAsync(EventBody<TestEvent>? message)
{
return Task.FromResult(true);
}
}
分組
消費者模式和事件模式都可以設定分組,在特性上設定了 Group
屬性,具有同一個分組的事件會被放到一個連線通道(RabbitMQ.Client.IConnection
)中,對於消費頻率不高的事件,複用連線通道可以有效較低資源消耗。
消費者模式分組示例:
[Consumer("ConsumerWeb_group_1", Qos = 1, Group = "group")]
public class Group_1_Consumer : IConsumer<GroupEvent>
{
public Task ExecuteAsync(EventBody<GroupEvent> message) => Task.CompletedTask;
public Task FaildAsync(Exception ex, int retryCount, EventBody<GroupEvent>? message) => Task.CompletedTask;
public Task<bool> FallbackAsync(EventBody<GroupEvent>? message) => Task.FromResult(true);
}
[Consumer("ConsumerWeb_group_2", Qos = 1, Group = "group")]
public class Group_2_Consumer : IConsumer<GroupEvent>
{
public Task ExecuteAsync(EventBody<GroupEvent> message) => Task.CompletedTask;
public Task FaildAsync(Exception ex, int retryCount, EventBody<GroupEvent>? message) => Task.CompletedTask;
public Task<bool> FallbackAsync(EventBody<GroupEvent>? message) => Task.FromResult(true);
}
事件匯流排模式分組示例:
[EventTopic("web1", Qos = 1, RetryFaildRequeue = true, Group = "group")]
public class Test1Event
{
public string Message { get; set; }
}
[EventTopic("web2", Qos = 1, RetryFaildRequeue = true, Group = "group")]
public class Test2Event
{
public string Message { get; set; }
}
消費者模式
消費者模式要求服務實現 IConsumer<TEvent>
介面,並新增 [Connsumer]
特性。
IConsumer<TEvent>
介面比較簡單,其定義如下:
public interface IConsumer<TEvent>
where TEvent : class
{
// 訊息處理.
public Task ExecuteAsync(EventBody<TEvent> message);
// ExecuteAsync 異常後立即執行此程式碼.
public Task FaildAsync(Exception ex, int retryCount, EventBody<TEvent>? message);
// 最後一次重試失敗時執行,用於補償.
public Task<bool> FallbackAsync(EventBody<TEvent>? message);
}
使用消費者模式時,需要先定義一個模型類,用於釋出者和消費者之間傳遞訊息,事件模型類只要是類即可,能夠正常序列化和反序列化,沒有其它要求。
public class TestEvent
{
public int Id { get; set; }
public override string ToString()
{
return Id.ToString();
}
}
然後繼承 IConsumer<TEvent>
介面實現消費者功能:
[Consumer("web1", Qos = 1)]
public class MyConsumer : IConsumer<TestEvent>
{
// 消費
public async Task ExecuteAsync(EventBody<TestEvent> message)
{
Console.WriteLine(message.Body.Id);
}
// 每次失敗時被執行
public async Task FaildAsync(Exception ex, int retryCount, EventBody<TestEvent>? message)
{
Console.WriteLine($"重試 {message.Body.Id},次數 {retryCount}");
await Task.CompletedTask;
}
// 最後一次失敗時執行
public async Task<bool> FallbackAsync(EventBody<TestEvent>? message)
{
Console.WriteLine($"最後一次 {message.Body.Id}");
// 如果返回 true,說明補償成功。
return true;
}
}
特性配置的說明請參考 消費者配置 。
消費、重試和補償
消費者收到伺服器推送的訊息時,ExecuteAsync
方法會被自動執行。當 ExecuteAsync
執行異常時,FaildAsync
方法會馬上被觸發,開發者可以利用 FaildAsync
記錄相關日誌資訊。
// 每次失敗時被執行
public async Task FaildAsync(Exception ex, int retryCount, EventBody<TestEvent>? message)
{
// 當 retryCount == -1 時,錯誤並非是 ExecuteAsync 方法導致的
if (retryCount == -1)
{
_logger.LogError(ex, "Consumer error,event id: {Id}", message?.Id);
// 可以在此處新增告警通知程式碼
await Task.Delay(1000);
}
else
{
_logger.LogError(ex, "Consumer exception,event id: {Id},retry count: {retryCount}", message!.Id, retryCount);
}
}
如果 FaildAsync
方法也出現異常時,不會影響整體流程,框架會等待到達間隔時間後繼續重試 ExecuteAsync
方法。
建議 FaildAsync
使用 try{}cathc{}
套住程式碼,不要對外丟擲異常,FaildAsync
的邏輯不要包含太多邏輯,並且 FaildAsync
只應記錄日誌或進行告警使用。
FaildAsync
被執行有一個額外情況,就是在消費訊息之前就已經發生錯誤,例如一個事件模型類有建構函式導致不能被反序列化,這個時候 FaildAsync
會被立即執行,且 retryCount = -1
。
當 ExecuteAsync
方法執行異常時,框架會自動重試,預設會重試五次,如果五次都失敗,則會執行 FallbackAsync
方法進行補償。
重試間隔時間會逐漸增大,請參考 重試。
當重試五次之後,就會立即啟動補償機制。
// 最後一次失敗時執行
public async Task<bool> FallbackAsync(EventBody<TestEvent>? message)
{
return true;
}
FallbackAsync
方法需要返回 bool,如果返回 true
,表示雖然 ExecuteAsync
出現異常,但是 FallbackAsync
補償後已經正常,該訊息會被正常消費掉。如果返回 false
,則說補償失敗,該訊息按照消費失敗處理。
只有 ExecuteAsync
異常時,才會觸發 FaildAsync
和 FallbackAsync
,如果是在處理訊息之前的異常,會直接失敗。
消費失敗
當 ExecuteAsync
失敗次數達到閾值時,並且 FallbackAsync
返回 false
,則該條訊息消費失敗,或者由於序列化等錯誤時直接失敗。
在 [Consumer]
特性中有三個很重要的配置:
public class ConsumerAttribute : Attribute
{
// 消費失敗次數達到條件時,是否放回佇列.
public bool RetryFaildRequeue { get; set; }
// 現異常時是否放回佇列,例如序列化錯誤等原因導致的,而不是消費時發生異常導致的.
public bool ExecptionRequeue { get; set; } = true;
// 繫結死信佇列.
public string? DeadQueue { get; set; }
}
當 ExecuteAsync
失敗次數達到閾值時,並且 FallbackAsync
返回 false
,則該條訊息消費失敗。
如果 RetryFaildRequeue == false
,那麼該條訊息會被 RabbitMQ 丟棄。
如果繫結了死信佇列,則會先推送到死信佇列,接著再丟棄。
如果 RetryFaildRequeue == true
,那麼該條訊息會被返回 RabbbitMQ 佇列中,等待下一次消費。
由於訊息失敗後會被放回佇列,因此繫結的死信佇列不會收到該訊息。
當序列化異常或者其它問題導致錯誤而不能進入 ExecuteAsync
方法時,FaildAsync
方法會首先被觸發一次,此時 retryCount 引數值為 -1
。
出現此種問題時,一般是開發者 bug 導致的,不會進行補償等操作,開發者可以在 FaildAsync
中處理該事件,記錄相關日誌資訊。
// 每次失敗時被執行,或者出現無法進入 ExecuteAsync 的異常
public async Task FaildAsync(Exception ex, int retryCount, EventBody<TestEvent>? message)
{
// 當 retryCount == -1 時,錯誤並非是 ExecuteAsync 方法導致的
if (retryCount == -1)
{
_logger.LogError(ex, "Consumer error,event id: {Id}", message?.Id);
// 可以在此處新增告警通知程式碼
await Task.Delay(1000);
}
else
{
_logger.LogError(ex, "Consumer exception,event id: {Id},retry count: {retryCount}", message!.Id, retryCount);
}
}
由於這種情況不妥善處理,會導致訊息丟失,因此框架預設將 ExecptionRequeue
設定為 true
,也就是說出現這種異常時,訊息會被放回佇列。如果問題一致沒有得到解決,則會出現迴圈:呼叫 FaildAsync
、放回佇列、呼叫 FaildAsync
、放回佇列... ...
所以應該在 FaildAsync
中新增程式碼通知開發者相關資訊,並且設定間隔時間,避免重試太頻繁。
自動建立佇列
框架預設會自動建立佇列,如果需要關閉自動建立功能,把 AutoQueueDeclare
設定為 false
即可。
builder.Services.AddMaomiMQ((MqOptionsBuilder options) =>
{
options.WorkId = 1;
options.AppName = "myapp";
options.AutoQueueDeclare = false;
options.Rabbit = (ConnectionFactory options) =>
{
options.HostName = "192.168.3.248";
options.ClientProvidedName = Assembly.GetExecutingAssembly().GetName().Name;
};
}, [typeof(Program).Assembly]);
當然還可以單獨為消費者配置是否自動建立佇列:
[Consumer("ConsumerWeb_create", AutoQueueDeclare = AutoQueueDeclare.Enable)]
預設情況下,關閉了全域性自動建立,則不會自動建立佇列。
如果關閉全域性自動建立,但是消費者配置了 AutoQueueDeclare = AutoQueueDeclare.Enable
,則還是會自動建立佇列。
如果消費者配置了 AutoQueueDeclare = AutoQueueDeclare.Disable
,則會忽略全域性配置,不會建立佇列。
Qos
讓程式需要嚴格根據順序消費時,可以使用 Qos = 1
,框架會嚴格保證逐條消費,如果程式不需要順序消費,希望可以快速處理所有訊息,則可以將 Qos 設定大一些。由於 Qos 和重試、補償機制組合使用會有多種情況,因此請參考 重試。
Qos 是透過特性來配置的:
[Consumer("ConsumerWeb", Qos = 1)]
可以透過調高 Qos 值,讓程式在可以併發訊息,提高併發量。
延遲佇列
延遲佇列有兩種,一種設定訊息過期時間,一種是設定佇列過期時間。
設定訊息過期時間,那麼該訊息在一定時間沒有被消費時,會被丟棄或移動到死信佇列中,該配置只對單個訊息有效,請參考 訊息過期。
佇列設定過期後,當訊息在一定時間內沒有被消費時,會被丟棄或移動到死信佇列中,該配置只對所有訊息有效。基於這一點,我們可以實現延遲佇列。
首先建立消費者,繼承 EmptyConsumer,那麼該佇列會在程式啟動時被建立,但是不會建立 IConnection 進行消費。然後設定佇列訊息過期時間以及繫結死信佇列,繫結的死信佇列既可以使用消費者模式實現,也可以使用事件模式實現。
[Consumer("ConsumerWeb_dead_2", Expiration = 6000, DeadQueue = "ConsumerWeb_dead_queue_2")]
public class EmptyDeadConsumer : EmptyConsumer<DeadEvent>
{
}
// ConsumerWeb_dead 消費失敗的訊息會被此消費者消費。
[Consumer("ConsumerWeb_dead_queue_2", Qos = 1)]
public class Dead_2_QueueConsumer : IConsumer<DeadQueueEvent>
{
// 消費
public Task ExecuteAsync(EventBody<DeadQueueEvent> message)
{
Console.WriteLine($"死信佇列,事件 id:{message.Id}");
return Task.CompletedTask;
}
// 每次失敗時被執行
public Task FaildAsync(Exception ex, int retryCount, EventBody<DeadQueueEvent>? message) => Task.CompletedTask;
// 最後一次失敗時執行
public Task<bool> FallbackAsync(EventBody<DeadQueueEvent>? message) => Task.FromResult(false);
}
空消費者
當識別到空消費者時,框架只會建立佇列,而不會啟動消費者消費訊息。
可以結合延遲佇列一起使用,該佇列不會有任何消費者,當該佇列的訊息過期時,都由死信佇列直接消費,示例如下:
[Consumer("ConsumerWeb_empty", Expiration = 6000, DeadQueue = "ConsumerWeb_empty_dead")]
public class MyEmptyConsumer : EmptyConsumer<TestEvent> { }
[Consumer("ConsumerWeb_empty_dead", Qos = 10)]
public class MyDeadConsumer : IConsumer<TestEvent>
{
public Task ExecuteAsync(EventBody<TestEvent> message) => Task.CompletedTask;
public Task FaildAsync(Exception ex, int retryCount, EventBody<TestEvent>? message) => Task.CompletedTask;
public Task<bool> FallbackAsync(EventBody<TestEvent>? message) => Task.FromResult(true);
}
對於跨程序的佇列,A 服務不消費只發布,B 服務負責消費,A 服務中可以加一個空消費者,保證 A 服務啟動時該佇列一定存在,另一方面,消費者服務不應該關注佇列的定義,也不太應該建立佇列。
分組
透過配置 Group
屬性將多個消費者放到同一個連線通道中執行,對於那些併發量不高的佇列,複用連線通道可以降低資源消耗。
示例:
[Consumer("ConsumerWeb_group_1", Qos = 1, Group = "group")]
public class Group_1_Consumer : IConsumer<GroupEvent>
{
}
[Consumer("ConsumerWeb_group_2", Qos = 1, Group = "group")]
public class Group_2_Consumer : IConsumer<GroupEvent>
{
}
事件匯流排模式
Maomi.MQ 內部設計了一個事件匯流排,可以幫助開發者實現事件編排、實現本地事務、正向執行和補償。
首先定義一個事件型別,該事件繫結一個 topic 或佇列,事件需要使用 [EventTopic]
標識,並設定該事件對於的佇列名稱。
[EventTopic]
特性擁有與 [Consumer]
相同的特性,可參考 [Consumer]
的使用配置事件,請參考 消費者配置。
[EventTopic("EventWeb")]
public class TestEvent
{
public string Message { get; set; }
public override string ToString()
{
return Message;
}
}
然後編排事件執行器,每個執行器都需要繼承 IEventHandler<T>
介面,然後使用 [EventOrder]
特性標記執行順序。
[EventOrder(0)]
public class My1EventEventHandler : IEventHandler<TestEvent>
{
public async Task CancelAsync(EventBody<TestEvent> @event, CancellationToken cancellationToken)
{
}
public async Task ExecuteAsync(EventBody<TestEvent> @event, CancellationToken cancellationToken)
{
Console.WriteLine($"{@event.Id},事件 1 已被執行");
}
}
[EventOrder(1)]
public class My2EventEventHandler : IEventHandler<TestEvent>
{
public async Task CancelAsync(EventBody<TestEvent> @event, CancellationToken cancellationToken)
{
}
public async Task ExecuteAsync(EventBody<TestEvent> @event, CancellationToken cancellationToken)
{
Console.WriteLine($"{@event.Id},事件 2 已被執行");
}
}
每個事件執行器都必須實現 IEventHandler<T>
介面,並且設定 [EventOrder]
特性以便確認事件的執行順序,框架會按順序執行 IEventHandler<T>
的 ExecuteAsync
方法,當 ExecuteAsync
出現異常時,則反向按順序呼叫 CancelAsync
。
由於程式可能隨時掛掉,因此透過 CancelAsync
實現補償是不太可能的,CancelAsync
主要作為記錄相關資訊而使用。
中介軟體
中介軟體的作用是便於開發者攔截事件、記錄資訊、實現本地事務等,如果開發者不配置,則框架會自動建立 DefaultEventMiddleware<TEvent>
型別作為該事件的中介軟體服務。
自定義事件中介軟體示例程式碼:
public class TestEventMiddleware : IEventMiddleware<TestEvent>
{
public async Task HandleAsync(EventBody<TestEvent> @event, EventHandlerDelegate<TestEvent> next)
{
await next(@event, CancellationToken.None);
}
}
next
委託是框架構建的事件執行鏈路,在中介軟體中可以攔截事件、決定是否執行事件鏈路。
在中介軟體中呼叫 next()
委託時,框架開始按順序執行事件,即前面提到的 My1EventEventHandler
、My2EventEventHandler
。
當一個事件有多個執行器時,由於程式可能會在任何時刻掛掉,因此本地事務必不可少。
例如,在中介軟體中注入資料庫上下文,然後啟動事務執行資料庫操作,當其中一個 EventHandler 執行失敗時,執行鏈路會回滾,同時不會提交事務。
可以參考 消費者模式 實現中介軟體的重試和補償方法。
示例如下:
public class TestEventMiddleware : IEventMiddleware<TestEvent>
{
private readonly BloggingContext _bloggingContext;
public TestEventMiddleware(BloggingContext bloggingContext)
{
_bloggingContext = bloggingContext;
}
public async Task HandleAsync(EventBody<TestEvent> @event, EventHandlerDelegate<TestEvent> next)
{
using (var transaction = _bloggingContext.Database.BeginTransaction())
{
await next(@event, CancellationToken.None);
await transaction.CommitAsync();
}
}
public Task FaildAsync(Exception ex, int retryCount, EventBody<TestEvent>? message)
{
return Task.CompletedTask;
}
public Task<bool> FallbackAsync(EventBody<TestEvent>? message)
{
return Task.FromResult(true);
}
}
[EventOrder(0)]
public class My1EventEventHandler : IEventHandler<TestEvent>
{
private readonly BloggingContext _bloggingContext;
public My1EventEventHandler(BloggingContext bloggingContext)
{
_bloggingContext = bloggingContext;
}
public async Task CancelAsync(EventBody<TestEvent> @event, CancellationToken cancellationToken)
{
Console.WriteLine($"{@event.Id} 被補償,[1]");
}
public async Task HandlerAsync(EventBody<TestEvent> @event, CancellationToken cancellationToken)
{
await _bloggingContext.Posts.AddAsync(new Post
{
Title = "魯濱遜漂流記",
Content = "隨便寫寫就對了"
});
await _bloggingContext.SaveChangesAsync();
}
}
[EventOrder(1)]
public class My2EventEventHandler : IEventHandler<TestEvent>
{
private readonly BloggingContext _bloggingContext;
public My2EventEventHandler(BloggingContext bloggingContext)
{
_bloggingContext = bloggingContext;
}
public async Task CancelAsync(EventBody<TestEvent> @event, CancellationToken cancellationToken)
{
Console.WriteLine($"{@event.Id} 被補償,[2]");
}
public async Task HandlerAsync(EventBody<TestEvent> @event, CancellationToken cancellationToken)
{
await _bloggingContext.Posts.AddAsync(new Post
{
Title = "紅樓夢",
Content = "賈寶玉初試雲雨情"
});
await _bloggingContext.SaveChangesAsync();
throw new OperationCanceledException("故意報錯");
}
}
事件執行時,如果出現異常,也是會被重試的,中介軟體 TestEventMiddleware 的 FaildAsync、FallbackAsync 會被依次執行。
你可以參考 消費者模式 或者 重試 。
分組消費
事件分組消費主要是利用同一個 IConnection 同時處理多個訊息佇列,提高通道利用率。
示例:
[EventTopic("EventGroup_1", Group = "aaa")]
public class Test1Event
{
public string Message { get; set; }
public override string ToString()
{
return Message;
}
}
[EventTopic("EventGroup_2", Group = "aaa")]
public class Test2Event
{
public string Message { get; set; }
public override string ToString()
{
return Message;
}
}
Maomi.MQ 的 IConsumer<T>
是一個消費者(一個佇列)使用一個 IConnection,預設情況下事件匯流排也是。
對於哪些併發量不大或利用率較低的佇列,可以透過事件分組將其合併到同一個 IConnection 中進行處理。
使用方法很簡單,只需要在定義事件時,配置 [EventTopic]
特性的 Group
方法即可。
由於不同佇列被放到一個 IConnection 中消費,如果事件都設定了 Qos,那麼框架會預設計算平均值,例如:
[EventTopic("web3_1", Group = "aaa", Qos = 10)]
public class Test1Event
[EventTopic("web3_2", Group = "aaa", Qos = 6)]
public class Test2Event
此時框架會設定 Qos 為 8
。
配置
在引入 Maomi.MQ 框架時,可以配置相關屬性,示例和說明如下:
// this.
builder.Services.AddMaomiMQ((MqOptionsBuilder options) =>
{
// 當前程式節點,用於配置分散式雪花 id
options.WorkId = 1;
// 是否自動建立佇列
options.AutoQueueDeclare = true;
// 當前應用名稱,用於標識訊息的釋出者和消費者程式
options.AppName = "myapp";
// RabbitMQ 配置
options.Rabbit = (ConnectionFactory options) =>
{
options.HostName = "192.168.3.248";
options.ClientProvidedName = Assembly.GetExecutingAssembly().GetName().Name;
};
}, [typeof(Program).Assembly]); // 要被掃描的程式集
消費者配置
消費者模式 [Consumer]
和事件匯流排模式 [EventTopic]
具有相同的屬性配置,其配置說明如下:
名稱 | 型別 | 預設值 | 說明 |
---|---|---|---|
Queue | string | 佇列名稱 | |
DeadQueue | string? | 繫結死信佇列名稱 | |
ExecptionRequeue | bool | true | 出現異常時是否放回佇列,例如序列化錯誤等原因導致的,而不是消費時發生異常導致的 |
Expiration | int | 0 | 佇列訊息過期時間,單位毫秒 |
Qos | ushort | 1 | Qos |
RetryFaildRequeue | bool | false | 消費失敗次數達到條件時,是否放回佇列 |
Group | string? | 分組名稱 | |
AutoQueueDeclare | AutoQueueDeclare | AutoQueueDeclare.None | 是否自動建立佇列 |
環境隔離
目前還在考慮要不要支援多租戶模式。
在開發中,往往需要在本地除錯,本地程式啟動後會連線到開發伺服器上,一個佇列收到訊息時,會向其中一個消費者推送訊息。那麼我本地除錯時,釋出一個訊息後,可能本地程式收不到該訊息,而是被開發環境中的程式消費掉了。
這個時候,我們希望可以將本地除錯環境跟開發環境隔離開來,可以使用 RabbitMQ 提供的 VirtualHost 功能。
首先透過 put 請求建立一個新的 VirtualHost,請參考文件:https://www.rabbitmq.com/docs/vhosts#using-http-api
然後在程式碼中配置 VirtualHost:
builder.Services.AddMaomiMQ((MqOptionsBuilder options) =>
{
options.WorkId = 1;
options.AutoQueueDeclare = true;
options.AppName = "myapp";
options.Rabbit = (ConnectionFactory options) =>
{
options.HostName = "192.168.3.248";
#if DEBUG
options.VirtualHost = "debug";
#endif
options.ClientProvidedName = Assembly.GetExecutingAssembly().GetName().Name;
};
}, [typeof(Program).Assembly]);
雪花 id 配置
Maomi.MQ.RabbitMQ 使用了 IdGenerator 生成雪花 id,使得每個事件在叢集中都有一個唯一 id。
框架透過 IIdFactory 介面建立雪花 id,你可以透過替換 IIdFactory
介面配置雪花 id 生成規則。
services.AddSingleton<IIdFactory>(new DefaultIdFactory((ushort)optionsBuilder.WorkId));
示例:
public class DefaultIdFactory : IIdFactory
{
/// <summary>
/// Initializes a new instance of the <see cref="DefaultIdFactory"/> class.
/// </summary>
/// <param name="workId"></param>
public DefaultIdFactory(ushort workId)
{
var options = new IdGeneratorOptions(workId) { SeqBitLength = 10 };
YitIdHelper.SetIdGenerator(options);
}
/// <inheritdoc />
public long NextId() => YitIdHelper.NextId();
}
IdGenerator 框架生成雪花 id 配置請參考:
https://github.com/yitter/IdGenerator/tree/master/C%23
Qos 併發和順序
基於消費者模式和基於事件模式都是透過特性來配置消費屬性,Qos 是其中一個重要的屬性。
Qos 場景
對於消費者模式和事件匯流排模式,在沒有使用 Group
屬性配置消費行為時,每個佇列都會獨佔一個 IConnection 以及 Host service。
對於消費頻率很高但是不能併發的佇列,最好不要設定 Group
屬性,以及務必設定 Qos = 1
。這樣依賴,該消費者會獨佔資源進行消費,在保證順序的情況下,獨佔資源有助於提高消費能力。
[Consumer("web1", Qos = 1)]
public class MyConsumer : IConsumer<TestEvent>
{
}
當需要需要提高消費吞吐量,而且不需要順序消費時,可以將 Qos 設定高一些,RabbitMQ Client 框架會透過預取等方式提高吞吐量,並且多條訊息可以併發消費。
如果判斷一些消費者的消費頻率不會很高時,可以將這些消費者放到一個分組中。
當多個消費者或事件配置共用一個分組時,那麼這些事件的 Qos 應當一致,否則按照平均值來算。
示例:
[Consumer("web1", Qos = 10, Group = "group")]
public class My1Consumer : IConsumer<TestEvent>
{
}
[Consumer("web2", Qos = 6, Group = "group")]
public class My2Consumer : IConsumer<TestEvent>
{
}
由於兩個消費者使用相同的分組,因此複用通道的 Qos 會被設定為 8。
如果消費頻率不高,但是需要順序消費時,可以將這些消費者放到同一個分組中,並且 Qos 設定為 1。
[Consumer("web1", Qos = 1, Group = "group1")]
public class My1Consumer : IConsumer<TestEvent>
{
}
[Consumer("web2", Qos = 1, Group = "group1")]
public class My2Consumer : IConsumer<TestEvent>
{
}
併發和異常處理
第一次情況,Qos 為 1 時,不設定 ExecptionRequeue 、RetryFaildRequeue。
第二種情況,Qos 為 1 時,設定 ExecptionRequeue 、RetryFaildRequeue。
Qos 為 1 時,會保證嚴格順序消費,ExecptionRequeue 、RetryFaildRequeue 會影響失敗的訊息是否會被放回佇列,如果放回佇列,下一次消費會繼續消費之前失敗的訊息。如果錯誤(如 bug)得不到解決,則會出現消費、失敗、放回佇列、重新消費這樣的迴圈。
第三次情況,Qos > 1 時,不設定 ExecptionRequeue 、RetryFaildRequeue。
第四種情況,Qos > 1 時,設定 ExecptionRequeue 、RetryFaildRequeue。
當 Qos 大於 1 時,如果設定了 RetryFaildRequeue = true
,那麼消費失敗的訊息會被放回佇列中,但是不一定下一次會立即重新消費該條訊息。
重試
重試時間
當消費者 ExecuteAsync
方法異常時,框架會進行重試,預設會重試五次,按照 2 作為指數設定重試時間間隔。
第一次失敗後,間隔 2 秒重試,第二次失敗後,間隔 4 秒,接著分別是 8、16、32 秒。
Maomi.MQ.RabbitMQ 使用了 Polly 框架做重試策略管理器,預設透過 DefaultRetryPolicyFactory 服務生成重試間隔策略。
DefaultRetryPolicyFactory 程式碼示例如下:
/// <summary>
/// Default retry policy.<br />
/// 預設的策略提供器.
/// </summary>
public class DefaultRetryPolicyFactory : IRetryPolicyFactory
{
/// <inheritdoc/>
public virtual Task<AsyncRetryPolicy> CreatePolicy(string queue, long id)
{
// Create a retry policy.
// 建立重試策略.
var retryPolicy = Policy
.Handle<Exception>()
.WaitAndRetryAsync(
retryCount: 5,
sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
onRetry: async (exception, timeSpan, retryCount, context) =>
{
_logger.LogDebug("Retry execution event,queue [{Queue}],retry count [{RetryCount}],timespan [{TimeSpan}]", queue, retryCount, timeSpan);
await FaildAsync(queue, exception, timeSpan, retryCount, context);
});
return Task.FromResult(retryPolicy);
}
public virtual Task FaildAsync(string queue, Exception ex, TimeSpan timeSpan, int retryCount, Context context)
{
return Task.CompletedTask;
}
}
你可以透過實現 IRetryPolicyFactory 介面,替換預設的重試策略服務服務。
services.AddSingleton<IRetryPolicyFactory, DefaultRetryPolicyFactory>();
重試機制
設定消費者程式碼如下:
[Consumer("web1", Qos = 1 , RetryFaildRequeue = true)]
public class MyConsumer : IConsumer<TestEvent>
{
private int _retryCount = 0;
// 消費
public async Task ExecuteAsync(EventBody<TestEvent> message)
{
Console.WriteLine($"執行 {message.Body.Id} 第幾次:{_retryCount} {DateTime.Now}");
_retryCount++;
throw new Exception("1");
}
// 每次失敗時被執行
public async Task FaildAsync(Exception ex, int retryCount, EventBody<TestEvent>? message)
{
Console.WriteLine($"重試 {message.Body.Id} 第幾次:{retryCount} {DateTime.Now}");
await Task.CompletedTask;
}
// 最後一次失敗時執行
public async Task<bool> FallbackAsync(EventBody<TestEvent>? message)
{
Console.WriteLine($"執行 {message.Body.Id} 補償 {DateTime.Now}");
return true;
}
}
}
首先會執行 IConsumer<TEvent>.ExecuteAsync()
或 IEventMiddleware<TEvent>.ExecuteAsync()
消費訊息,此時 ExecuteAsync()
執行失敗,立即觸發 FaildAsync()
函式。
然後等待一段時間間隔後,接著會重新執行 ExecuteAsync()
方法。
比如預設重試機制是重試五次,那麼最終 IConsumer<TEvent>.ExecuteAsync()
或 IEventMiddleware<TEvent>.ExecuteAsync()
都會被執行 6次,一次正常消費和五次重試消費。
FallbackAsync()
方法會在最後一次重試失敗後被呼叫,該函式要返回一個 bool 型別。
當多次重試失敗後,框架會呼叫 FallbackAsync 方法,如果該方法放回 true,那麼框架會認為雖然 ExecuteAsync()
執行失敗,但是透過 FallbackAsync()
已經補償好了,該訊息會被當做正常完成消費,框架會向 RabbitMQ 伺服器傳送 ACK,接著消費下一條訊息。
如果 FallbackAsync()
返回 true,框架會認為該訊息徹底失敗,如果設定了 RetryFaildRequeue = true
,那麼該條訊息會被放回訊息佇列,等待下一次消費。否則該條訊息會被直接丟棄。
持久化剩餘重試次數
當消費者處理訊息失敗時,預設消費者會重試 5 次,如果已經重試了 3 次,此時程式重啟,那麼下一次消費該訊息時,依然是繼續重試五次。
需要記憶重試次數,在程式重啟時,能夠按照剩餘次數進行重試。
引入 Maomi.MQ.RedisRetry 包。
配置示例:
builder.Services.AddMaomiMQ((MqOptionsBuilder options) =>
{
options.WorkId = 1;
options.AutoQueueDeclare = true;
options.AppName = "myapp";
options.Rabbit = (ConnectionFactory options) =>
{
options.HostName = "192.168.3.248";
options.ClientProvidedName = Assembly.GetExecutingAssembly().GetName().Name;
};
}, [typeof(Program).Assembly]);
builder.Services.AddMaomiMQRedisRetry((s) =>
{
ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("192.168.3.248");
IDatabase db = redis.GetDatabase();
return db;
});
預設 key 只會保留 5 分鐘。也就是說,如果五分鐘之後程式才重新消費該訊息,那麼就會剩餘重試次數就會重置。
死信佇列
死信佇列
可以給一個消費者或事件繫結死信佇列,當該佇列的訊息失敗後並且不會放回佇列時,該訊息會被推送到死信佇列中,示例:
[Consumer("ConsumerWeb_dead", Qos = 1, DeadQueue = "ConsumerWeb_dead_queue", RetryFaildRequeue = false)]
public class DeadConsumer : IConsumer<DeadEvent>
{
// 消費
public Task ExecuteAsync(EventBody<DeadEvent> message)
{
Console.WriteLine($"事件 id:{message.Id}");
throw new OperationCanceledException();
}
// 每次失敗時被執行
public Task FaildAsync(Exception ex, int retryCount, EventBody<DeadEvent>? message) => Task.CompletedTask;
// 最後一次失敗時執行
public Task<bool> FallbackAsync(EventBody<DeadEvent>? message) => Task.FromResult(false);
}
// ConsumerWeb_dead 消費失敗的訊息會被此消費者消費。
[Consumer("ConsumerWeb_dead_queue", Qos = 1)]
public class DeadQueueConsumer : IConsumer<DeadQueueEvent>
{
// 消費
public Task ExecuteAsync(EventBody<DeadQueueEvent> message)
{
Console.WriteLine($"死信佇列,事件 id:{message.Id}");
return Task.CompletedTask;
}
// 每次失敗時被執行
public Task FaildAsync(Exception ex, int retryCount, EventBody<DeadQueueEvent>? message) => Task.CompletedTask;
// 最後一次失敗時執行
public Task<bool> FallbackAsync(EventBody<DeadQueueEvent>? message) => Task.FromResult(false);
}
如果使用死信佇列,則務必將 RetryFaildRequeue
設定為 false,那麼消費者會在重試多次失敗後,向 RabbitMQ 傳送 nack 訊號,RabbitMQ 就會將該訊息轉發到繫結的死信佇列中。
延遲佇列
建立一個消費者,繼承 EmptyConsumer,那麼該佇列會在程式啟動時被建立,但是不會建立 IConnection 進行消費。然後設定佇列訊息過期時間以及繫結死信佇列,繫結的死信佇列既可以使用消費者模式實現,也可以使用事件模式實現。
[Consumer("ConsumerWeb_dead_2", Expiration = 6000, DeadQueue = "ConsumerWeb_dead_queue_2")]
public class EmptyDeadConsumer : EmptyConsumer<DeadEvent>
{
}
// ConsumerWeb_dead 消費失敗的訊息會被此消費者消費。
[Consumer("ConsumerWeb_dead_queue_2", Qos = 1)]
public class Dead_2_QueueConsumer : IConsumer<DeadQueueEvent>
{
// 消費
public Task ExecuteAsync(EventBody<DeadQueueEvent> message)
{
Console.WriteLine($"事件 id:{message.Id} 已到期");
return Task.CompletedTask;
}
// 每次失敗時被執行
public Task FaildAsync(Exception ex, int retryCount, EventBody<DeadQueueEvent>? message) => Task.CompletedTask;
// 最後一次失敗時執行
public Task<bool> FallbackAsync(EventBody<DeadQueueEvent>? message) => Task.FromResult(false);
}
例如,使用者下單之後,如果 15 分鐘之內沒有付款,那麼訊息到期時,自動取消訂單。
可觀測性
功能還在繼續完善中。請參考 ActivitySourceApi 示例。
為了快速部署可觀測性平臺,可以使用 OpenTelemetry 官方提供的示例包快速部署相關的服務。
下載示例倉庫原始碼:
git clone https://github.com/open-telemetry/opentelemetry-demo.git
由於示例中會包含大量的 demo 微服務,因此我們需要開啟 docker-compose.yml 檔案,將 services 節點的 Core Demo Services
和 Dependent Services
服務直接刪除,只保留可觀測性元件。或者直接點選下載筆者已經修改好的版本: docker-compose.yml
執行命令部署可觀測性服務:
docker-compose up -d
opentelemetry-collector-contrib 用於收集鏈路追蹤的可觀測性資訊,有 grpc 和 http 兩種,監聽埠如下:
Port | Protocol | Endpoint | Function |
---|---|---|---|
4317 | gRPC | n/a | Accepts traces in OpenTelemetry OTLP format (Protobuf). |
4318 | HTTP | /v1/traces |
Accepts traces in OpenTelemetry OTLP format (Protobuf and JSON). |
經過容器埠對映後,對外埠可能不是 4317、4318 了。
引入 Maomi.MQ.Instrumentation 包,以及其它相關 OpenTelemetry 包。
<PackageReference Include="Maomi.MQ.Instrumentation " Version="1.1.0" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.8.1" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.8.1" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.8.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.8.1" />
然後注入服務:
const string serviceName = "myapp";
builder.Services.AddMaomiMQ((MqOptionsBuilder options) =>
{
options.WorkId = 1;
options.AutoQueueDeclare = true;
options.AppName = serviceName;
options.Rabbit = (ConnectionFactory options) =>
{
options.HostName = "192.168.3.248";
options.ClientProvidedName = Assembly.GetExecutingAssembly().GetName().Name;
};
}, [typeof(Program).Assembly]);
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService(serviceName))
.WithTracing(tracing =>
{
tracing.AddMaomiMQInstrumentation(options =>
{
options.Sources.AddRange(MaomiMQDiagnostic.Sources);
options.RecordException = true;
})
.AddAspNetCoreInstrumentation()
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri("http://127.0.0.1:32772/v1/traces");
options.Protocol = OtlpExportProtocol.HttpProtobuf;
});
});
啟動服務後,進行釋出、消費,鏈路追蹤資訊會被自動推送到 OpenTelemetry Collector 中,透過 Jaeger 、Skywalking 等元件可以讀取出來。
由於 publish、consumer 屬於兄弟 trace 而不是同一個 trace,因此需要透過 Tags 查詢相關聯的 trace,格式 event.id=xxx
。