الگوی Outbox و داستان یک راهکار هوشمندانه در پستگرس

اخیراً مقالهای از صادق دوستی در Dev.to خواندم که نشان داد با تجربه و تسلط، میتوان برای چالشهای بزرگ، راهحلهایی هوشمندانه و ساده پیدا کرد. یعنی در دنیای فنی، گاهی غرق پیچیدگیها میشویم و راهحلهای ساده اما عمیق را نادیده میگیریم. این پست ادای دینی است به صادق عزیز Sadeq Dousti و مقالات ارزشمندش، و مروری بر مشکل پیادهسازی الگوی Outbox با PostgreSQL در حجم بالای داده و راهحلی خلاقانه برای آن.
https://dev.to/msdousti/postgresql-outbox-pattern-revamped-part-1-3lai
🎯 الگوی Outbox چیست؟
در یک فروشگاه آنلاین، ثبت سفارش باید چند کار را انجام دهد:
✅ذخیره در پایگاه داده
✅ارسال ایمیل تأیید
✅بهروزرسانی موجودی
✅اطلاع به واحد ارسال
این اکشنها به بروکرهایی مثل Kafka ارسال میشوند تا هر واحد کار خود را انجام دهد.
❓ اگر ارسال پیام به بروکر با خطا مواجه شود؟
Outbox وارد میشود! سفارش در پایگاه داده ذخیره شده و یک پیام در جدول Outbox ثبت میشود. یک سرویس جداگانه پیامها را خوانده و به بروکر میفرستد. در صورت خطا، پیام در جدول باقی میماند تا دوباره برای پردازش ارسال شود اما …
🔍 چالش: حجم بالای دادهها
با افزایش پیامها در Outbox:
⚠️کوئریهای خواندن پیامهای منتشرنشده کند میشوند.
⚠️ایندکسها به دلیل آپدیتهای مکرر غیربهینه میشوند.
⚠️مصرف منابع سیستم افزایش مییابد.
💡 راهحل: پارتیشنبندی هوشمند
صادق دوستی پیشنهاد میکند جدول Outbox را به دو پارتیشن تقسیم کنیم:
outbox_unpublished: پیامهای منتشرنشده (published_at IS NULL)
outbox_published: پیامهای منتشرشده (published_at NOT NULL)
با این کار، پیامهای جدید به outbox_unpublished میروند و پس از انتشار، بهصورت خودکار به outbox_published منتقل میشوند. بنابراین کوئریها فقط روی پارتیشن سبکتر اجرا میشوند.
🎉 مزایا:
✅سرعت بالا: کوئریها روی پارتیشن کوچکتر اجرا میشوند.
✅مدیریت آسان: حذف پیامهای قدیمی با TRUNCATE سریع است.
✅بهینهسازی منابع: ایندکسها کوچک و کارآمد میمانند.
⚠️ ملاحظات فنی در پیادهسازی
در کنار مزایای پارتیشنبندی جدول Outbox، ملاحظاتی نیز وجود دارد که در شرایط خاص میتواند عملکرد این راهکار را تحت تأثیر قرار دهد:
- نرخ بالای جابجایی بین پارتیشنها
یکی از چالشهای محتمل این است که در بسیاری از سیستمها، بیشتر پیامهای درجشده در Outbox در نهایت منتشر میشوند. این موضوع منجر به جابجایی مکرر دادهها از پارتیشن پیامهای منتشرنشده به پارتیشن پیامهای منتشرشده میشود. از آنجا که در PostgreSQL عملیات جابجایی بین پارتیشنها در واقع معادل یکDELETEاز پارتیشن مبدا وINSERTدر پارتیشن مقصد است، این فرآیند میتواند سربار قابلتوجهی داشته باشد، بهویژه در سیستمهای پرتراکنش. - اصل طراحی پارتیشنها در برابر پویایی دادهها
پارتیشنبندی زمانی بیشترین کارایی را دارد که رکوردها پس از درج در یک پارتیشن باقی بمانند. در سناریوی Outbox، تغییر وضعیت رکوردها منجر به انتقال بین پارتیشنها میشود که خلاف این اصل است و ممکن است در حجم بالا، باعث افت عملکرد یا پیچیدگیهای عملیاتی شود. - تعدد وضعیتهای پیام
در بسیاری از سیستمها، پیامها ممکن است بیش از دو وضعیت داشته باشند (برای مثال: در انتظار، در حال ارسال، ارسال موفق، خطا و غیره). در این شرایط، استفاده از پارتیشنبندی بر اساس وضعیت ممکن است باعث افزایش تعداد پارتیشنها و پیچیدگی در نگهداری و کوئرینویسی شود. همچنین، ترکیب این مسئله با نیاز به Atomicity در تراکنشهای مرتبط میتواند مدیریت دادهها را دشوارتر کند.
✅ پاسخ به چالشها
در پاسخ به این ملاحظات، راهکار پیشنهادی به صورت زیر بهینه شده است:
- پارتیشن پیامهای منتشرشده میتواند به صورت
UNLOGGEDو بدون ایندکس تعریف شود تا سربار نوشتن کاهش یابد و عملیات درج پیامها در آن سبک و سریع باشد. - در پارتیشن پیامهای منتشرنشده، به دلیل محدود بودن حجم داده و تمرکز کوئریها فقط روی این بخش، ایندکسها کوچک و کارآمد باقی میمانند.
- عملیات انتقال بین پارتیشنها میتواند با فرآیندهای سبکشده و زمانبندیشده (Batch) انجام شود تا از فشار لحظهای بر سیستم جلوگیری شود.
در نهایت، پیادهسازی این الگو باید با توجه به حجم داده، نرخ تراکنش، ساختار سامانه و نیازمندیهای خاص هر پروژه بررسی و تست شود. این راهکار در سناریوهایی با بار پردازشی متوسط و نیاز به بهینهسازی خواندن پیامهای منتشرنشده، میتواند عملکرد مناسبی ارائه دهد.
🏁 جمعبندی
الگوی Outbox برای هماهنگی سیستمهای توزیعشده عالی است، اما پیادهسازی نادرست آن مشکلساز میشود. پارتیشنبندی هوشمند صادق دوستی این الگو را بهینهتر و سریعتر میکند.
🔗 برای جزئیات بیشتر، حتا مقاله صادق در Dev.to را بخوانید!