پیادهسازی الگوی Outbox 📬
در سیستمهای توزیعشده، ما اغلب با چالش همگامسازی دیتابیس و سیستمهای خارجی مواجه هستیم. تصور کنید سفارشی را در دیتابیس ذخیره کرده و سپس پیامی را به یک message broker منتشر میکنید. اگر هر یک از این عملیاتها شکست بخورد، سیستم شما در وضعیتی ناسازگار (inconsistent) قرار میگیرد.
الگوی Outbox این مشکل را با در نظر گرفتن انتشار پیام به عنوان بخشی از تراکنش دیتابیس شما حل میکند. به جای انتشار مستقیم پیامها، ما آنها را در یک جدول Outbox در دیتابیس خود ذخیره میکنیم و از عملیات اتمیک اطمینان حاصل میکنیم. سپس یک فرآیند جداگانه این پیامها را به طور قابل اعتماد منتشر میکند.
در این مقاله، ما به پیادهسازی این الگو در NET.، از راهاندازی تا مقیاسپذیری، خواهیم پرداخت.
چرا به الگوی Outbox نیاز داریم؟ 🤔
الگوی transactional Outbox یک مشکل رایج را در سیستمهای توزیعشده حل میکند. این مشکل زمانی رخ میدهد که شما باید دو کار را همزمان انجام دهید: ذخیره داده و ارتباط با یک کامپوننت خارجی.
برای مثال، یک میکروسرویس را تصور کنید که باید:
🔹️ یک سفارش جدید را در دیتابیس خود ذخیره کند.
🔹️ در مورد این سفارش جدید به سیستمهای دیگر اطلاع دهد.
🔹️ اگر یکی از این مراحل شکست بخورد، سیستم شما میتواند در وضعیتی ناسازگار قرار گیرد.
// کد با مشکل سازگاری بالقوه
public async Task<OrderDto> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
// ...
await unitOfWork.CommitAsync(cancellationToken);
// تراکنش دیتابیس در این نقطه کامل شده است
// 💥 اگر برنامه اینجا کرش کند یا event bus در دسترس نباشد چه؟
await eventBus.Send(new OrderCreatedIntegrationEvent(order.Id));
return new OrderDto { Id = order.Id, Total = order.Total };
}
الگوی transactional Outbox به حل این مشکل کمک میکند. 📈 ما هم سفارش و هم پیام Outbox را در یک تراکنش دیتابیس واحد ذخیره میکنیم. این یک عملیات "همه یا هیچ" است.
سپس یک پردازشگر Outbox جداگانه، ارسال واقعی پیام را مدیریت میکند. ⚠️ نکته مهمی که باید در اینجا متوجه شوید این است که الگوی Outbox به ما تحویل حداقل-یکباره (at-least-once delivery) را میدهد. این یعنی ما باید مصرفکنندگان پیام خود را idempotent بسازیم.
پیادهسازی الگوی Outbox 💾
ابتدا، جدول Outbox خود را ایجاد میکنیم:
CREATE TABLE outbox_messages (
id UUID PRIMARY KEY,
type VARCHAR(255) NOT NULL,
content JSONB NOT NULL,
occurred_on_utc TIMESTAMP WITH TIME ZONE NOT NULL,
processed_on_utc TIMESTAMP WITH TIME ZONE NULL,
error TEXT NULL
);
و کلاس #C برای نمایش ورودی Outbox:
public sealed class OutboxMessage
{
public Guid Id { get; init; }
public string Type { get; init; }
public string Content { get; init; }
public DateTime OccurredOnUtc { get; init; }
public DateTime? ProcessedOnUtc { get; init; }
public string? Error { get; init; }
}
✨️یک رویکرد زیبا برای پیادهسازی این، استفاده از domain events برای نمایش نوتیفیکیشنها است. قبل از تکمیل تراکنش، میتوانید تمام رویدادها را برداشته و آنها را به عنوان پیامهای Outbox ذخیره کنید.