در سیستمهای توزیعشده و رویدادمحور، اطمینان از ارسال دقیق و قابلاعتماد پیامها یکی از حیاتیترین چالشهاست. فرض کنید تراکنشی در پایگاه داده با موفقیت انجام شده اما رویداد مرتبط با آن به Kafka یا هر Message Broker دیگری ارسال نشده است — در این حالت، بخشی از سیستم از تغییرات باخبر نمیشود و ناسازگاری دادهها رخ میدهد. برای جلوگیری از چنین وضعیتهایی، الگوی Outbox Pattern معرفی شد؛ رویکردی که امکان ارسال مطمئن رویدادها همراه با تراکنش دیتابیس را فراهم میکند و تضمین میدهد هیچ پیام مهمی در مسیر از دست نرود.
مشکل اصلی
فرض کنید در یک سرویس سفارش (OrderService) پس از ثبت سفارش، باید پیامی برای سرویس انبار (InventoryService) ارسال شود:
_dbContext.Orders.Add(order);
await _dbContext.SaveChangesAsync();
await _kafkaProducer.ProduceAsync("order-created", order);
اگر ارسال پیام به Kafka بعد از ثبت در دیتابیس انجام شود و بین این دو عملیات خطا رخ دهد (مثلاً شبکه قطع شود)، داده در دیتابیس ثبت شده اما پیام در Kafka منتشر نشده است. برای حل این مشکل، الگوی Outbox معرفی شده است.
راهحل Outbox Pattern
در این الگو، بهجای انتشار مستقیم پیام بعد از تراکنش، ابتدا رویداد در جدولی بهنام Outbox ذخیره میشود — در همان تراکنش دیتابیس.
ساختار جدول Outbox معمولاً به این شکل است:
| Id | EventType | Payload | CreatedAt | Processed |
|----|-------------|----------------------------------------------|----------------------|-----------|
| 1 | OrderCreated | { "OrderId": 123, "UserId": 45 } | 2025-10-29 | false |
وقتی تراکنش دیتابیس با موفقیت انجام شد، دادهی رویداد نیز بهصورت اتمیک ذخیره شده است. سپس، یک پردازشگر پسزمینه (Background Worker) در بازههای زمانی کوتاه (مثلاً هر ۵ ثانیه)
این جدول را بررسی میکند، پیامهای پردازشنشده را به Kafka ارسال میکند، و ستون Processed را به true تغییر میدهد.
مزایای Outbox
- ارسال اتمیک و قابلاعتماد: رویدادها دقیقاً همزمان با تراکنش ذخیره میشوند.
- قابلیت Retry: اگر Kafka در دسترس نباشد، Worker مجدداً تلاش میکند.
- عدم از دست رفتن پیامها حتی در صورت Crash شدن سرویس.
- قابل تست و مانیتورینگ: جدول Outbox بهعنوان audit log قابل بررسی است.
معماری کلی
+---------------------------+
| Application Service |
|---------------------------|
| Save Entity (DB Tx) |
| Insert Outbox Event |
+------------+--------------+
|
| (DB Commit)
v
+-----------------+
| Outbox Table |
| (unprocessed) |
+-----------------+
|
| (Polling)
v
+-----------------+
| Outbox Worker |
| Publish to Kafka|
+-----------------+
پیادهسازی ساده در .NET
برای ذخیره رویداد در Outbox:
await _dbContext.OutboxEvents.AddAsync(new OutboxEvent
{
EventType = "OrderCreated",
Payload = JsonSerializer.Serialize(order),
CreatedAt = DateTime.UtcNow,
Processed = false
});
await _dbContext.SaveChangesAsync();
و در یک BackgroundService جداگانه:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var events = await _dbContext.OutboxEvents
.Where(e => !e.Processed)
.Take(50)
.ToListAsync();
foreach (var evt in events)
{
await _kafkaProducer.ProduceAsync("order-events", evt.Payload);
evt.Processed = true;
}
await _dbContext.SaveChangesAsync();
await Task.Delay(5000, stoppingToken);
}
}
نکات پیشرفته
- برای بار زیاد، میتوان Outbox را به Kafka Connect + Debezium متصل کرد تا پیامها بدون نیاز به polling، مستقیماً از تغییرات دیتابیس منتشر شوند.
- میتوان جدول Outbox را partition کرد تا حجم داده زیاد باعث کندی نشود.
- در سیستمهای چندسرویسی، هر سرویس Outbox مخصوص خود دارد.
جمعبندی
الگوی Outbox یکی از ستونهای اصلی در طراحی سیستمهای رویدادمحور قابلاعتماد است. این الگو تضمین میکند که هیچ پیامی در اثر خطای ناگهانی از دست نرود و هماهنگی بین دیتابیس و سیستم پیامرسانی حفظ شود.
ادامه در مقاله بعدی: در گام بعد، به سراغ الگوی Inbox Pattern خواهیم رفت تا نحوهی مدیریت دریافت مطمئن و جلوگیری از پردازش تکراری رویدادها را بررسی کنیم.