پردازش Outbox ⚙️
پردازشگر Outbox کامپوننت بعدی است که به آن نیاز داریم. این میتواند یک فرآیند فیزیکی جداگانه یا یک background worker در همان فرآیند باشد.
من از Quartz برای زمانبندی background jobها برای پردازش Outbox استفاده خواهم کرد.
[DisallowConcurrentExecution]
public class OutboxProcessorJob(...) : IJob
{
public async Task Execute(IJobExecutionContext context)
{
// پیامهای پردازش نشده را از دیتابیس واکشی میکند
var messages = await connection.QueryAsync<OutboxMessage>(...);
foreach (var message in messages)
{
try
{
// پیام را deserialize کرده و به message broker منتشر میکند
await publishEndpoint.Publish(deserializedMessage);
// پیام را به عنوان پردازش شده علامتگذاری میکند
await connection.ExecuteAsync(
"UPDATE outbox_messages SET processed_on_utc = @ProcessedOnUtc WHERE id = @Id",
...);
}
catch (Exception ex)
{
// خطا را لاگ میکند
}
}
await transaction.CommitAsync();
}
}
یک راه جایگزین برای پردازش پیامهای Outbox، استفاده از Transaction log tailing است. ما میتوانیم این را با استفاده از Postgres logical replication پیادهسازی کنیم.
ملاحظات و مزایا و معایب 🧐
الگوی Outbox، ضمن کارآمدی، پیچیدگی و نوشتنهای اضافی در دیتابیس را به همراه دارد.
🔹 توصیه میکنم مکانیزمهای تلاش مجدد (retry) را در پردازشگر Outbox برای بهبود قابلیت اطمینان پیادهسازی کنید.
🔹 ضروری است که مصرفکنندگان پیام idempotent را پیادهسازی کنید.
🔹 با گذشت زمان، جدول Outbox میتواند به طور قابل توجهی رشد کند. مهم است که یک استراتژی آرشیو از همان ابتدا پیادهسازی کنید.
مقیاسپذیری پردازش Outbox 🚀
با رشد سیستم شما، ممکن است یک پردازشگر Outbox نتواند حجم پیامها را مدیریت کند.
🔹 یک رویکرد ساده، افزایش فرکانس اجرای job پردازشگر Outbox است.
🔹 یک استراتژی مؤثر دیگر، افزایش اندازه بچ (batch size) هنگام واکشی پیامهای پردازش نشده است.
🔹 برای سیستمهای با حجم بالا، پردازش Outbox به صورت موازی میتواند بسیار مؤثر باشد. یک مکانیزم قفلگذاری برای ادعای بچهای پیام پیادهسازی کنید.
جمعبندی ✅
الگوی Outbox یک ابزار قدرتمند برای حفظ سازگاری داده در سیستمهای توزیعشده است. با جداسازی عملیات دیتابیس از انتشار پیام، الگوی Outbox تضمین میکند که سیستم شما حتی در مواجهه با شکستها، قابل اعتماد باقی بماند.
به یاد داشته باشید که مصرفکنندگان خود را idempotent نگه دارید، استراتژیهای مقیاسپذیری مناسب را پیادهسازی کنید، و رشد جدول Outbox خود را مدیریت کنید.
🔖 هشتگها:
#SoftwareArchitecture #SystemDesign #Microservices #DistributedSystems #OutboxPattern #EventDrivenArchitecture #DataConsistency
💾 ابطال کش در سیستمهای توزیع شده (Cache Invalidation)
در یک پروژه اخیر، من یک چالش رایج در سیستمهای توزیع شده را حل کردم: همگام نگه داشتن کشها. 🧩 ما از یک رویکرد کشینگ دو سطحی استفاده میکردیم:
کش in-memory روی هر وب سرور برای دسترسی فوقسریع.
کش مشترک Redis برای جلوگیری از ضربه زدن مکرر به پایگاه داده.
مشکل این بود که وقتی دادهای در پایگاه داده تغییر میکرد، به راهی نیاز داشتیم تا به سرعت به تمام وب سرورها بگوییم که کشهای in-memory خود را پاک کنند. اینجا بود که Redis Pub/Sub به کمک آمد. ما یک کانال Redis را به طور خاص برای پیامهای ابطال کش راهاندازی کردیم. 📣
💻 پیادهسازی CacheInvalidationBackgroundService
هر برنامه یک سرویس CacheInvalidationBackgroundService را اجرا میکند که در کانال ابطال کش عضو میشود (Subscribe میکند):
public class CacheInvalidationBackgroundService(
IServiceProvider serviceProvider)
: BackgroundService
{
public const string Channel = "cache-invalidation";
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await subscriber.SubscribeAsync(Channel, (channel, key) =>
{
var cache = serviceProvider.GetRequiredService<IMemoryCache>();
cache.Remove(key);
return Task.CompletedTask;
});
}
}
هر زمان که دادهای در پایگاه داده تغییر میکند، ما یک پیام را در این کانال با کلید کش داده بهروز شده، منتشر میکنیم. تمام وب سرورها در این کانال عضو هستند، بنابراین فوراً از حذف داده قدیمی از کشهای in-memory خود مطلع میشوند. از آنجایی که کش in-memory در صورت از کار افتادن برنامه پاک میشود، از دست دادن پیامهای ابطال کش مشکلی ایجاد نمیکند. این کار کشهای ما را سازگار نگه میدارد و تضمین میکند که کاربران ما همیشه بهروزترین اطلاعات را میبینند. ✅
📝 خلاصه
Redis Pub/Sub
یک راهحل جادویی برای هر نیاز پیامرسانی نیست، اما سادگی و سرعت آن، آن را به یک ابزار ارزشمند تبدیل میکند. کانالها به ما امکان میدهند تا به راحتی ارتباط بین مؤلفههای با اتصال سست (loosely coupled components) را پیادهسازی کنیم. 🔗
کانالهای Redis دارای معناشناسی "حداکثر یک بار تحویل" (at-most-once delivery) هستند، بنابراین برای مواردی که از دست دادن گاهبهگاه پیام قابل قبول است، بهترین گزینه میباشند.
موفق باشید! 👋
🔖 هشتگها:
#Redis #PubSub #CacheInvalidation #DistributedSystems #DotNet #Messaging #BackgroundService
معمولاً تنها در موارد حیاتی یا غیرقابل بازگشت مثل پرداختها یا provisioning حسابها به این میزان از اطمینان نیاز است. 💳
این تصمیم بستگی به سطح اطمینان مورد نیاز دارد:
اگر تکرار عملیات عواقب واقعی دارد (مالی یا دادهای)، باید idempotency را صریحاً اعمال کنید.
در غیر این صورت، retry کردن عملیات ممکن است قابلقبول باشد.
همهی consumerها به سربار بررسیهای idempotency نیاز ندارند.
اگر عملیات شما بهطور طبیعی idempotent است، میتوانید از جدول اضافه و تراکنش صرفنظر کنید.
بهعنوان مثال:
بهروزرسانی projectionها 📊
تنظیم flag وضعیت ✅
یا refresh کردن cache 🧩
همگی نمونههایی از عملیات deterministic هستند که اجرای چندبارهی آنها خطری ندارد.
مثل:
«تنظیم وضعیت کاربر روی Active» یا «بازسازی Read Model» — اینها state را بازنویسی میکنند نه اینکه چیزی به آن اضافه کنند.
برخی از handlerها هم از Precondition Check برای جلوگیری از تکرار استفاده میکنند.
اگر handler در حال بهروزرسانی یک entity باشد، ابتدا میتواند بررسی کند که آیا entity در وضعیت مورد نظر هست یا نه؛ اگر هست، بهسادگی return کند.
این محافظ ساده در بسیاری موارد کافی است.
⚠️ الگوی Idempotent Consumer را بدون فکر در همهجا اعمال نکنید.
فقط در جایی از آن استفاده کنید که از آسیب واقعی (مالی یا ناسازگاری دادهای) جلوگیری کند.
برای سایر موارد، سادگی بهتر است.
سیستمهای توزیعشده ذاتاً غیرقابلپیشبینی هستند. ⚙️ Retryها، پیامهای تکراری (duplicates) و خرابیهای جزئی (partial failures) بخش طبیعی عملکرد آنها محسوب میشوند.
نمیتوانی از وقوعشان جلوگیری کنی، اما میتوانی سیستم را طوری طراحی کنی که کمترین تأثیر را از آنها بگیرد. 💪
از قابلیت Message Deduplication داخلی در Broker خود استفاده کن تا پیامهای تکراری از سمت Producer حذف شوند.
در سمت Consumer، الگوی Idempotent Consumer Pattern را اعمال کن تا مطمئن شوی Side Effectها فقط یکبار رخ میدهند حتی در صورت Retry شدن پیامها. 🔁
همیشه رکورد پیامهای پردازششده و اثر واقعی آنها را در یک تراکنش واحد ذخیره کن.
این کار کلید حفظ Consistency در سیستم توزیعشده است. 🧱
نه هر Message Handlerی نیاز به این الگو دارد.
اگر Consumer شما ذاتاً Idempotent است یا میتواند با یک Precondition ساده پردازش را زود متوقف کند، نیازی به پیچیدگی اضافی نیست. 🚫
اما هرجایی که عملیات باعث تغییر Persistent State یا فراخوانی سیستمهای خارجی میشود، Idempotency دیگر یک انتخاب نیست — بلکه تنها راه تضمین Consistency است. ✅
سیستم خود را طوری بساز که Retryها را تحمل کند.
در این صورت، سیستم توزیعشدهات بسیار قابلاعتمادتر خواهد شد. 🔐
نکتهی جالب اینجاست که وقتی این اصل را واقعاً درک میکنی، آن را در همهی سیستمهای واقعی دنیا میبینی. 🌍
امیدوارم این مطلب برات مفید بوده باشه 💙
⚖️ The Trade-Off
این تصمیم بستگی به سطح اطمینان مورد نیاز دارد:
اگر تکرار عملیات عواقب واقعی دارد (مالی یا دادهای)، باید idempotency را صریحاً اعمال کنید.
در غیر این صورت، retry کردن عملیات ممکن است قابلقبول باشد.
🧠 When Idempotent Consumer Isn’t Needed
همهی consumerها به سربار بررسیهای idempotency نیاز ندارند.
اگر عملیات شما بهطور طبیعی idempotent است، میتوانید از جدول اضافه و تراکنش صرفنظر کنید.
بهعنوان مثال:
بهروزرسانی projectionها 📊
تنظیم flag وضعیت ✅
یا refresh کردن cache 🧩
همگی نمونههایی از عملیات deterministic هستند که اجرای چندبارهی آنها خطری ندارد.
مثل:
«تنظیم وضعیت کاربر روی Active» یا «بازسازی Read Model» — اینها state را بازنویسی میکنند نه اینکه چیزی به آن اضافه کنند.
برخی از handlerها هم از Precondition Check برای جلوگیری از تکرار استفاده میکنند.
اگر handler در حال بهروزرسانی یک entity باشد، ابتدا میتواند بررسی کند که آیا entity در وضعیت مورد نظر هست یا نه؛ اگر هست، بهسادگی return کند.
این محافظ ساده در بسیاری موارد کافی است.
⚠️ الگوی Idempotent Consumer را بدون فکر در همهجا اعمال نکنید.
فقط در جایی از آن استفاده کنید که از آسیب واقعی (مالی یا ناسازگاری دادهای) جلوگیری کند.
برای سایر موارد، سادگی بهتر است.
🧭 Takeaway
سیستمهای توزیعشده ذاتاً غیرقابلپیشبینی هستند. ⚙️ Retryها، پیامهای تکراری (duplicates) و خرابیهای جزئی (partial failures) بخش طبیعی عملکرد آنها محسوب میشوند.
نمیتوانی از وقوعشان جلوگیری کنی، اما میتوانی سیستم را طوری طراحی کنی که کمترین تأثیر را از آنها بگیرد. 💪
از قابلیت Message Deduplication داخلی در Broker خود استفاده کن تا پیامهای تکراری از سمت Producer حذف شوند.
در سمت Consumer، الگوی Idempotent Consumer Pattern را اعمال کن تا مطمئن شوی Side Effectها فقط یکبار رخ میدهند حتی در صورت Retry شدن پیامها. 🔁
همیشه رکورد پیامهای پردازششده و اثر واقعی آنها را در یک تراکنش واحد ذخیره کن.
این کار کلید حفظ Consistency در سیستم توزیعشده است. 🧱
نه هر Message Handlerی نیاز به این الگو دارد.
اگر Consumer شما ذاتاً Idempotent است یا میتواند با یک Precondition ساده پردازش را زود متوقف کند، نیازی به پیچیدگی اضافی نیست. 🚫
اما هرجایی که عملیات باعث تغییر Persistent State یا فراخوانی سیستمهای خارجی میشود، Idempotency دیگر یک انتخاب نیست — بلکه تنها راه تضمین Consistency است. ✅
سیستم خود را طوری بساز که Retryها را تحمل کند.
در این صورت، سیستم توزیعشدهات بسیار قابلاعتمادتر خواهد شد. 🔐
نکتهی جالب اینجاست که وقتی این اصل را واقعاً درک میکنی، آن را در همهی سیستمهای واقعی دنیا میبینی. 🌍
امیدوارم این مطلب برات مفید بوده باشه 💙
🔖هشتگها:
#IdempotentConsumer #Idempotency #DistributedSystems #MessageBroker
Forwarded from Sonora.Dev
🛠 حل مشکل Double Booking در سیستمهای رزرو
تمام پلتفرمهای رزرو مدرن با چالش Double Booking روبرو هستند: وقتی دو یا چند کاربر بهطور همزمان تلاش میکنند یک منبع محدود را رزرو کنند.
این مشکل، یک race condition است که میتواند اعتماد کاربر را نابود کند و برای سیستمهای پرترافیک، بحرانی است.
1️⃣ Pessimistic Locking
مکانیزم: قفل روی رکورد دیتابیس (SELECT ... FOR UPDATE)
مزایا: تضمین Consistency، جلوگیری از race condition
معایب: Throughput محدود، Deadlock Risk، مقیاسپذیری پایین
مناسب برای: Low-traffic / کمرقابت (مثل Web Check-in هواپیما)
2️⃣ Optimistic Locking
مکانیزم: بدون قفل، با استفاده از Versioning
مزایا: عملکرد خواندن بالا، افزایش concurrency
معایب: Conflict و Retry در High Contention، افزایش load روی DB
مناسب برای: Moderate traffic و منابع کمرقابت (رزرو هتل، رستوران)
3️⃣ In-Memory Distributed Locking
مکانیزم: Lock توزیعشده در Redis / In-Memory Cache
مزایا: کاهش فشار روی دیتابیس، High Concurrency، Low Latency
معایب: پیچیدگی زیرساخت، مدیریت crash و expiration، ریسک Lock ناتمام
مناسب برای: Popular events با 1K–10K RPS
4️⃣ Virtual Waiting Queue
مکانیزم: Async Queue + Backpressure + FIFO
مزایا:
محافظت از دیتابیس و cache در برابر surge
بهبود تجربه کاربری و fairness
مقیاسپذیری بسیار بالا (High Throughput)
معایب: پیچیدگی عملیاتی، نیاز به SSE یا WebSocket برای اطلاعرسانی
مناسب برای: Ultra High Traffic events (کنسرتها، فیلمهای بلاکباستر)
✅ جمعبندی فنی
هیچ راهحل واحدی برای همه سناریوها وجود ندارد
انتخاب معماری به الگوی ترافیک، سطح رقابت و محدودیت منابع وابسته است
سیستمهای High-Traffic باید Lock-free + Async + Fair Queue داشته باشند تا Tail Latency و double booking کنترل شود
Monitoring، Retry Policies و Backpressure، اجزای کلیدی در طراحی سیستم رزرو مقیاسپذیر هستند
#SystemDesign #DistributedSystems #Scalability #Concurrency #BackendArchitecture #HighTraffic #BookingSystems #Microservices #Queueing
تمام پلتفرمهای رزرو مدرن با چالش Double Booking روبرو هستند: وقتی دو یا چند کاربر بهطور همزمان تلاش میکنند یک منبع محدود را رزرو کنند.
این مشکل، یک race condition است که میتواند اعتماد کاربر را نابود کند و برای سیستمهای پرترافیک، بحرانی است.
1️⃣ Pessimistic Locking
مکانیزم: قفل روی رکورد دیتابیس (SELECT ... FOR UPDATE)
مزایا: تضمین Consistency، جلوگیری از race condition
معایب: Throughput محدود، Deadlock Risk، مقیاسپذیری پایین
مناسب برای: Low-traffic / کمرقابت (مثل Web Check-in هواپیما)
2️⃣ Optimistic Locking
مکانیزم: بدون قفل، با استفاده از Versioning
مزایا: عملکرد خواندن بالا، افزایش concurrency
معایب: Conflict و Retry در High Contention، افزایش load روی DB
مناسب برای: Moderate traffic و منابع کمرقابت (رزرو هتل، رستوران)
3️⃣ In-Memory Distributed Locking
مکانیزم: Lock توزیعشده در Redis / In-Memory Cache
مزایا: کاهش فشار روی دیتابیس، High Concurrency، Low Latency
معایب: پیچیدگی زیرساخت، مدیریت crash و expiration، ریسک Lock ناتمام
مناسب برای: Popular events با 1K–10K RPS
4️⃣ Virtual Waiting Queue
مکانیزم: Async Queue + Backpressure + FIFO
مزایا:
محافظت از دیتابیس و cache در برابر surge
بهبود تجربه کاربری و fairness
مقیاسپذیری بسیار بالا (High Throughput)
معایب: پیچیدگی عملیاتی، نیاز به SSE یا WebSocket برای اطلاعرسانی
مناسب برای: Ultra High Traffic events (کنسرتها، فیلمهای بلاکباستر)
✅ جمعبندی فنی
هیچ راهحل واحدی برای همه سناریوها وجود ندارد
انتخاب معماری به الگوی ترافیک، سطح رقابت و محدودیت منابع وابسته است
سیستمهای High-Traffic باید Lock-free + Async + Fair Queue داشته باشند تا Tail Latency و double booking کنترل شود
Monitoring، Retry Policies و Backpressure، اجزای کلیدی در طراحی سیستم رزرو مقیاسپذیر هستند
#SystemDesign #DistributedSystems #Scalability #Concurrency #BackendArchitecture #HighTraffic #BookingSystems #Microservices #Queueing