17.9 — 8. MassTransit / NServiceBus / EasyNetQ
Ba thư viện này nằm ở ba mức trừu tượng khác nhau, và chọn sai mức tốn kém hơn chọn sai sản phẩm. MassTransit là lựa chọn mặc định của phần lớn dự án .NET: abstraction dày, có sẵn outbox, saga, retry hai tầng — nhưng hãy kiểm tra điều khoản giấy phép của phiên bản bạn định dùng trước khi cam kết, vì mô hình cấp phép của dự án đã thay đổi theo thời gian. NServiceBus mạnh, tài liệu vào loại tốt nhất trong hệ sinh thái, và thu phí thương mại một cách minh bạch từ đầu. EasyNetQ mỏng, chỉ RabbitMQ, không có saga hay outbox — nhưng đôi khi mỏng chính là thứ bạn cần. Điều quan trọng hơn cả ba lựa chọn: giữ code nghiệp vụ không biết gì về thư viện bạn chọn, để đổi thư viện chỉ phải viết lại lớp hạ tầng.
Mục tiêu bài học
Sau bài này bạn có thể:
- So sánh ba thư viện theo mức trừu tượng, tính n ăng và chi phí.
- Biết kiểm tra giấy phép trước khi đưa thư viện vào dự án dài hạn.
- Cấu hình MassTransit với RabbitMQ ở mức đủ dùng cho production.
- Giữ code nghiệp vụ không phụ thuộc thư viện messaging.
- Nhận ra khi nào không cần thư viện nào cả.
Nội dung bài học
17.9.1 — Ba mức trừu tượng
| MassTransit | NServiceBus | EasyNetQ | |
|---|---|---|---|
| Mức trừu tượng | Cao | Cao | Thấp |
| Broker hỗ trợ | RabbitMQ, Azure SB, SQS, Kafka | RabbitMQ, Azure SB, SQS, MSMQ | Chỉ RabbitMQ |
| Outbox tích hợp | Có | Có | Không |
| Saga / state machine | Có | Có | Không |
| Retry hai tầng | Có | Có | Cơ bản |
| Scheduling | Có | Có | Không |
| Chi phí | Kiểm tra theo phiên bản | Thương mại | Miễn phí (MIT) |
| Độ dốc học | Trung bình | Cao | Thấp |
| Tài liệu | Tốt | Rất tốt | Đủ dùng |
Ba dòng đáng chú ý nhất: broker hỗ trợ (EasyNetQ khoá bạn vào RabbitMQ), saga (nếu cần điều phối quy trình nhiều bước thì EasyNetQ loại khỏi danh sách), và chi phí.
17.9.2 — Về giấy phép: kiểm tra trước khi cam kết
Đây là rủi ro ít người tính tới khi chọn thư viện: mô hình cấp phép của dự án mã nguồn mở có thể thay đổi. Một thư viện miễn phí hôm nay có thể yêu cầu trả phí ở phiên bản sau, và khi đó bạn đứng trước ba lựa chọn đều không dễ chịu: trả tiền, ở lại phiên bản cũ không còn được cập nhật, hoặc viết lại lớp hạ tầng messaging.
MassTransit là ví dụ cụ thể: mô hình cấp phép của nó đã có thay đổi giữa các phiên bản lớn. Hãy đọc trang giấy phép chính thức của đúng phiên bản bạn định dùng trước khi đưa vào dự án dài hạn, đừng dựa vào bài viết cũ hay thông tin truyền miệng.
Ba việc nên làm cho mọi thư viện nền tảng, không riêng messaging:
- Đọc giấy phép của phiên bản cụ thể, không phải của repo nói chung.
- Ước lượng chi phí thoát — nếu ngày mai phải bỏ thư viện này, bao nhiêu file phải sửa?
- Giữ abstraction mỏng của riêng bạn để câu trả lời cho câu hỏi trên là "vài file", không phải "cả dự án".
Điểm 3 là thứ bạn kiểm soát được, và mục 17.9.5 nói cách làm.
17.9.3 — MassTransit ở mức đủ dùng
builder.Services.AddMassTransit(x =>
{
x.AddConsumer<LeadConvertedConsumer>();
// Outbox: message chi publish khi transaction commit
x.AddEntityFrameworkOutbox<AppDbContext>(o =>
{
o.QueryDelay = TimeSpan.FromSeconds(5);
o.UseSqlServer();
o.UseBusOutbox();
});
x.UsingRabbitMq((context, cfg) =>
{
cfg.Host(builder.Configuration.GetConnectionString("RabbitMq"));
cfg.ReceiveEndpoint("lead-converted", e =>
{
e.PrefetchCount = 16; // gioi han message chua ACK
e.UseMessageRetry(r => r.Immediate(3)); // loi chop nhoang
e.UseScheduledRedelivery(r => r.Intervals( // loi keo dai
TimeSpan.FromMinutes(1),
TimeSpan.FromMinutes(5),
TimeSpan.FromMinutes(15)));
e.ConfigureConsumer<LeadConvertedConsumer>(context);
});
});
});
Cấu hình này đã có ba thứ cần cho production: outbox chống mất tin (bài 17.4), retry hai tầng (bài 17.6), và prefetch để phân bổ tải đều giữa consumer (bài 17.4).
Saga là thứ khó tự viết nhất và là lý do chính đáng nhất để dùng thư viện:
public sealed class OrderStateMachine : MassTransitStateMachine<OrderState>
{
public State AwaitingPayment { get; private set; } = null!;
public State Completed { get; private set; } = null!;
public OrderStateMachine()
{
InstanceState(x => x.CurrentState);
Initially(
When(OrderSubmitted)
.Then(c => c.Saga.SubmittedAtUtc = DateTime.UtcNow)
.TransitionTo(AwaitingPayment));
During(AwaitingPayment,
When(PaymentReceived)
.TransitionTo(Completed)
.Finalize(),
When(PaymentTimeout)
.Publish(c => new OrderCancelledEvent(c.Saga.CorrelationId))
.Finalize());
}
}
Trạng thái của saga được lưu bền vào database, nên nó sống sót qua restart. Tự viết đúng phần này — kể cả timeout, đồng thời và phục hồi — tốn nhiều công hơn người ta tưởng (saga pattern).
17.9.4 — EasyNetQ: khi mỏng là đủ
using var bus = RabbitHutch.CreateBus("host=localhost;username=guest;password=guest");
await bus.PubSub.PublishAsync(new LeadConvertedEvent(leadId, customerId));
await bus.PubSub.SubscribeAsync<LeadConvertedEvent>("billing", async evt =>
{
await billingService.CreateSubscriptionAsync(evt.CustomerId);
});
Ba dòng, chạy được ngay, MIT, không có gì để hiểu thêm.
Đánh đổi: không outbox, không saga, không scheduling, không retry hai tầng — bạn tự viết hoặc không có. Với một service nhỏ chỉ publish vài loại event đơn giản, đó là đánh đổi hợp lý. Với hệ thống có quy trình nhiều bước, bạn sẽ dần tự viết lại những gì MassTransit đã có, chỉ tệ hơn.