3️⃣ Publish/Subscribe Pattern 📣📡(بخش سوم)
ءPublish/Subscribe Pattern یک الگوی پیامرسانی غیرهمزمان است که در آن Publisherها پیامها یا Eventها را به یک Message Broker یا Event Bus مرکزی ارسال میکنند، بدون اینکه بدانند چه کسی مصرفکننده آنهاست.
در مقابل، Subscriberها علاقهمندی خود را به انواع خاصی از پیامها ثبت میکنند و بهصورت خودکار هنگام انتشار آنها را دریافت میکنند. این الگو منجر به یک Event-Driven Architecture با Coupling بسیار کم میشود.
🔍 تفاوت اصلی Publish/Subscribe با Point To Point Async Integration
ءPublish/Subscribe فرض میکند که چندین Subscriber میتوانند برای یک نوع Event وجود داشته باشند
ءPoint To Point Async Integration فرض میکند که فقط یک Subscriber برای هر نوع پیام وجود دارد
🔧 How it works:
🔹️ءPublisherها پیامها یا Eventها را به Topic یا Channelهای Message Broker ارسال میکنند بدون اینکه از Subscriberها اطلاعی داشته باشند
🔸️ءMessage Broker پیامها را دریافت، ذخیره و توزیع میکند
🔹️ءSubscriberها علاقهمندی خود را به Topic یا Event Type خاص ثبت میکنند
🔸️با انتشار یک پیام، Broker نسخهای از آن را به تمام Subscriberهای فعال ارسال میکند
🔹️چندین Subscriber میتوانند همزمان و مستقل یک پیام یکسان را پردازش کنند
🔸️این الگو از ارتباط One-to-Many پشتیبانی میکند
🔹️اضافه یا حذف Subscriberها نیازی به تغییر در Publisher ندارد
🔸️امکان Message Filtering وجود دارد تا هر Subscriber فقط پیامهای مرتبط را دریافت کند
✅ Benefits:
• ءDecoupling کامل بین Publisher و Subscriber
• ءScalability بالا با پردازش موازی پیامها توسط چند Subscriber
• مناسب برای Event-Driven Architecture
• افزودن قابلیتهای جدید فقط با اضافه کردن Subscriber جدید
• ءResilience بهتر؛ خطای یک Subscriber روی بقیه تأثیر ندارد
❌ Drawbacks:
• ءMessage Broker یک وابستگی مهم است و میتواند Single Point of Failure باشد
• ءDebugging و Tracing سختتر بهدلیل ماهیت غیرهمزمان
• چالشهای Eventual Consistency
• نیاز به طراحی دقیق برای Ordering پیامها و Duplicate Handling
🎯 Use cases:
🔹️سیستمهای Event-Driven که چند سرویس باید به یک Event واکنش نشان دهند
🔸️ءReal-time Notification (چت، داشبوردها، مانیتورینگ)
🔹️ءMicroservices برای همگامسازی دادهها از طریق Integration Events
🔸️سناریوهایی که سرویسهای جدید باید بدون تغییر Publisher به Eventها گوش دهند
🔹️ءWorkflowهایی که با یک Event چند مرحله در سرویسهای مختلف فعال میشوند
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
بهترین رهبران پر سروصدا نیستند.
برخی از مورد اعتمادترین افراد در جمع، کمترین صحبت را میکنند.
شما این نوع افراد را میشناسید:
🔹️حرفشان را عملی میکنند و به قول خود پایبندند
🔸️قبل از صحبت کردن با دقت گوش میدهند
🔹️برای دیگران فضا ایجاد میکنند بدون اینکه دنبال اعتبار باشند
🔸️آنها در میان آشوب، آرامش میآورند. نیازی ندارند قدرت یا نفوذ خود را نشان دهند.
آنها این اعتماد را به دست آوردهاند.
اغلب ما رهبر بودن را با کاریزما، دیدهشدن یا جسارت مرتبط میکنیم، اما قابل اعتماد بودن، تواضع و شایستگی، توان رهبر بودن را چند برابر میکند.
اگر میخواهید بدون داشتن عنوان، رهبری کنید، ابتدا با کسی باشید که دیگران بتوانند روی او حساب کنند، بهویژه زمانی که اوضاع پیچیده و نامنظم است.
برخی از مورد اعتمادترین افراد در جمع، کمترین صحبت را میکنند.
شما این نوع افراد را میشناسید:
🔹️حرفشان را عملی میکنند و به قول خود پایبندند
🔸️قبل از صحبت کردن با دقت گوش میدهند
🔹️برای دیگران فضا ایجاد میکنند بدون اینکه دنبال اعتبار باشند
🔸️آنها در میان آشوب، آرامش میآورند. نیازی ندارند قدرت یا نفوذ خود را نشان دهند.
آنها این اعتماد را به دست آوردهاند.
اغلب ما رهبر بودن را با کاریزما، دیدهشدن یا جسارت مرتبط میکنیم، اما قابل اعتماد بودن، تواضع و شایستگی، توان رهبر بودن را چند برابر میکند.
اگر میخواهید بدون داشتن عنوان، رهبری کنید، ابتدا با کسی باشید که دیگران بتوانند روی او حساب کنند، بهویژه زمانی که اوضاع پیچیده و نامنظم است.
4️⃣ Outbox Pattern 📤📦(بخش چهارم)
ءOutbox Pattern با ذخیرهکردن Eventها در یک جدول دیتابیس (Outbox) در همان Transaction تغییرات دادههای بیزینسی، انتشار قابلاعتماد Eventها را تضمین میکند.
سپس یک فرآیند جداگانه، Eventها را از Outbox خوانده و به Message Broker منتشر میکند.
به این شکل تضمین میشود که Eventها اگر و فقط اگر Transaction بیزینسی موفق باشد منتشر شوند.
🔧 How it works:
🔹️وقتی یک سرویس داده بیزینسی را تغییر میدهد، همزمان رکورد Event را در جدول Outbox و در همان Transaction دیتابیس درج میکند
🔸️ءTransaction دیتابیس، Atomicity بین تغییر داده و ایجاد Event را تضمین میکند
🔹️یک فرآیند Background یا Worker بهصورت مداوم جدول Outbox را برای Eventهای منتشرنشده بررسی میکند
🔸️این فرآیند Eventها را خوانده و به Message Broker ارسال میکند
🔹️بعد از انتشار موفق، Event بهعنوان پردازششده علامتگذاری یا از Outbox حذف میشود
🔸️اگر انتشار شکست بخورد، Event در Outbox باقی میماند و بهصورت خودکار Retry میشود
🔹️این الگو مشکل Dual-Write (ذخیره داده موفق، اما انتشار Event ناموفق) را حذف میکند
🔸️ءEventها حداقل یکبار (At-Least-Once) منتشر میشوند و Consumerها باید Duplicateها را مدیریت کنند
✅ Benefits:
• تضمین میکند Eventها فقط در صورت موفقیت Transaction بیزینسی منتشر شوند
• مشکل Dual-Write بین دیتابیس و Message Broker را بهطور کامل حذف میکند
• یک Audit Trail قابلاعتماد از تمام Eventهای سیستم در دیتابیس فراهم میکند
• امکان Event Replay و Recovery با نگهداشتن تاریخچه Eventها
❌ Drawbacks:
• ایجاد Eventual Consistency چون Eventها بلافاصله منتشر نمیشوند و Polling دارند
• نیاز به زیرساخت اضافی برای Outbox Processor و مانیتورینگ آن
• ایجاد سربار عملکردی بهدلیل نوشتنهای اضافی در دیتابیس و Polling
• نیاز به مدیریت Duplicate Eventها در Consumer (میتوان از InBox Pattern + Idempotence استفاده کرد)
🎯 Use cases:
🔹️ءMicroservices که باید انتشار Event بعد از تغییر دادهها را قطعاً تضمین کنند
🔹️سیستمهای Event Sourcing که هر تغییر وضعیت باید بهعنوان Event ثبت شود
🔹️هر سناریویی که Consistency بین سرویسها حیاتی است و از دست رفتن پیام غیرقابلقبول است
🧠 4️⃣2️⃣ سؤال مهم مصاحبه برای Software Architect
اگر قراره در نقش Software Architect مصاحبه بدی (یا مصاحبه بگیری)، این سؤالها فقط دانش فنی رو نمیسنجن؛
بلکه طرز فکر معماری، تصمیمگیری و تجربهی واقعی تو رو محک میزنن.
1️⃣ چطور بین Monolith، Modular Monolith و Microservices برای یک سیستم جدید تصمیم میگیری؟
2️⃣ ءTrade-off بین Layered Architecture، Vertical Slice و Hexagonal Architecture چیه؟
3️⃣ چطور اصل Principle of Least Surprise رو در طراحی کامپوننتها رعایت میکنی؟
4️⃣ موقع Scale کردن سیستم، چطور جلوی Accidental Complexity رو میگیری؟
5️⃣ چه زمانی Synchronous و چه زمانی Asynchronous Communication رو انتخاب میکنی؟
6️⃣ چطور Idempotent Operation طراحی میکنی وقتی سیستم Retry داره؟
7️⃣ در یک Workflow با throughput بالا، چطور از Race Condition جلوگیری میکنی؟
8️⃣ چه زمانی از Queue، چه زمانی از Stream و چه زمانی از Direct Call استفاده میکنی؟
9️⃣ نقش Saga Pattern در workflowهای طولانی چیه؟
🔟 چطور سیستم رو برای Exactly-once یا Effectively-once processing طراحی میکنی؟
1️⃣1️⃣ با Partial Failure در سیستمهای توزیعشده چطور برخورد میکنی؟
2️⃣1️⃣ چه Patternهایی به حفظ Consistency بین چند سرویس کمک میکنن؟
3️⃣1️⃣ چطور سیستمی طراحی میکنی که در برابر Failure وابستگیهای خارجی دوام بیاره؟
4️⃣1️⃣ رویکردت برای تشخیص و ایزوله کردن سرویسهای کند چیه؟
5️⃣1️⃣ چطور Caching Layer (L1/L2) طراحی میکنی که هم Stale Data نداشته باشه هم Thundering Herd ایجاد نکنه؟
6️⃣1️⃣ چه نشانههایی میگه سیستم به Vertical Scaling نیاز داره یا Horizontal Scaling؟
7️⃣1️⃣ چطور Read-heavy workload طراحی میکنی برای بیشترین throughput؟
8️⃣1️⃣ چه Patternهایی باعث کاهش Load دیتابیس میشن بدون ضربه زدن به Consistency؟
9️⃣1️⃣ با مشکل Hot Partition چطور برخورد میکنی؟
0️⃣2️⃣ سیستمهای با Write Volume بالا رو چطور مدیریت میکنی؟
1️⃣2️⃣ چطور بین Relational Database و Document Database انتخاب میکنی؟
2️⃣2️⃣ چه زمانی Distributed Transaction و چه زمانی Eventual Consistency؟
3️⃣2️⃣ چطور یک سیستم Audit-friendly طراحی میکنی بدون اینکه Performance نابود بشه؟
4️⃣2️⃣ استراتژیت برای Schema Evolution بدون Downtime چیه؟
🎯 جمعبندی
این سؤالها دنبال جواب کتابی نیستن.
مصاحبهکننده میخواد بدونه:
• چطور فکر میکنی
• چطور تصمیم میگیری
• چطور با Trade-offها کنار میای
اگه بتونی پشت هر جواب، تجربه یا منطق داشته باشی، یعنی واقعاً Architect هستی نه فقط عنوانش رو داری.
چگونه با GitHub Actions و NET. یک CI/CD Pipeline بسازیم
آیا میخواهید فرآیند توسعه نرمافزار خود را سادهتر کنید و چرخههای انتشار را سریعتر انجام دهید؟ 🚀
تصور کنید بتوانید با هر تغییر کد، برنامههای NET. خود را بهصورت خودکار build، test و deploy کنید.
با CI/CD میتوانید بهطور قابلتوجهی کارهای دستی را کاهش دهید و تمرکز بیشتری روی ساخت نرمافزار داشته باشید، در نتیجه انتشارهای سریعتر و قابلاعتمادتری خواهید داشت.
و شروع CI/CD هیچوقت به این اندازه آسان نبوده است.
ءGitHub Actions کاملاً رایگان و ساده برای استفاده هستند. ✅
بنابراین، در این مطلب موارد زیر را بررسی میکنیم:
• معرفی CI/CD و GitHub Actions
• ساخت pipeline برای build و test در NET.
• ساخت pipeline برای deployment روی Azure App Service
ءContinuous Integration و Continuous Delivery چیست؟
قبل از اینکه به GitHub Actions بپردازیم، سعی میکنم بهصورت خلاصه توضیح بدهم CI/CD چیست.
ءCI/CD روشی است برای افزایش دفعات تحویل قابلیتهای جدید، با اضافه کردن اتوماسیون به workflow توسعه نرمافزار.
ءContinuous Integration یا «CI» به فرآیند خودکار همگامسازی کد جدید با repository اشاره دارد. هر تغییر جدید در کد برنامه بلافاصله build، test و merge میشود.
ءContinuous Delivery یا Deployment یا «CD» به خودکارسازی بخش deployment از workflow اشاره دارد. زمانی که تغییری ایجاد میکنید که در repository merge میشود، این مرحله مسئول deploy کردن آن تغییرات روی محیط production (یا هر محیط دیگری) است.
ءContinuous Integration با GitHub Actions
اگر از GitHub استفاده میکنید، شروع Continuous Integration هیچوقت به این راحتی نبوده است.
میتوانید از GitHub Actions برای خودکارسازی pipeline مربوط به build، test و deployment استفاده کنید. میتوانید workflowهایی بسازید که هر commit روی repository شما را build و test کنند، یا زمانی که یک tag جدید ساخته میشود، deploy به production انجام دهند.
برای ساخت یک GitHub Action، شما یک workflow مینویسید که هنگام وقوع یک event خاص در repository اجرا شود. نمونهای از این eventها شامل commit روی branch اصلی، ایجاد یک tag یا اجرای دستی workflow است.
در ادامه یک workflow در GitHub Actions برای build و test یک پروژه NET. آورده شده است:
name: Build & Test 🧪
on:
push:
branches:
- main
env:
DOTNET_VERSION: '7.0.x'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET 📦
uses: actions/setup-dotnet@v3
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Install dependencies 📂
run: dotnet restore WebApi
- name: Build 🧱
run: dotnet build WebApi --configuration Release --no-restore
- name: Test 🧪
run: dotnet test WebApi --configuration Release --no-build
بیایید ببینیم اینجا دقیقاً چه اتفاقی میافتد 🔍
• تعریف یک event برای trigger کردن workflow ⚡️
• راهاندازی NET SDK. با نسخهای که از env.DOTNET_VERSION خوانده میشود 📦
• ءRestore، build و test کردن پروژه با استفاده از ابزار dotnet CLI 🧪🧱
میتوانید همین امروز این workflow را به repository گیتهاب خود اضافه کنید 🧑💻 و بهمحض commit کردن کد، بازخورد فوری دریافت کنید 🚀
وقتی اجرای workflow به دلیل خطای build یا شکست تستها fail شود ❌، یک ایمیل اعلان دریافت خواهید کرد 📧
ءContinuous Delivery به Azure با GitHub Actions ☁️
ءContinuous Integration نقطه شروع بسیار خوبی برای CI/CD است، اما ارزش واقعی زمانی مشخص میشود که فرآیند deployment را خودکار کنید 🤖
این سناریو را تصور کنید 👇
• شما تغییری در کد ایجاد میکنید ✏️
• ءcommit باعث trigger شدن pipeline deployment میشود 🔄
• چند دقیقه بعد، تغییرات شما در production در دسترس هستند 🌍
معمولاً موضوع کمی پیچیدهتر است، چون باید به پیکربندیها، migration دیتابیس و موارد دیگر هم فکر کنیم ⚙️🗄
اما سعی کنید تصویر کلی را ببینید 🧠
اگر برنامه خود را در cloud اجرا میکنید، مثلاً روی Azure ☁️، به احتمال زیاد یک GitHub Action آماده برای این کار وجود دارد که میتوانید از آن استفاده کنید.
در ادامه یک deployment pipeline آورده شده که من برای انتشار برنامهام روی Azure App Service استفاده میکنم 🚀
name: Publish 🚀
on:
push:
branches:
- main
env:
AZURE_WEBAPP_NAME: web-api
AZURE_WEBAPP_PACKAGE_PATH: './publish'
DOTNET_VERSION: '7.0.x'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET 📦
uses: actions/setup-dotnet@v3
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Build and Publish 📂
run: |
dotnet restore WebApi
dotnet build WebApi -c Release --no-restore
dotnet publish WebApi -c Release --no-build
--output '${{ env.AZURE_WEBAPP_PACKAGE_PATH }}'
- name: Deploy to Azure 🌌
uses: azure/webapps-deploy@v2
with:
app-name: ${{ env.AZURE_WEBAPP_NAME }}
publish-profile: ${{ secrets.AZURE_PUBLISH_PROFILE }}
package: '${{ env.AZURE_WEBAPP_PACKAGE_PATH }}'
این workflow شباهت زیادی به workflow قبلی دارد، با این تفاوتها 🔍
اضافه شدن مرحله publish و پیکربندی مسیر خروجی 📤
استفاده از اکشن azure/webapps-deploy@v2 برای deploy روی Azure ☁️
اگر نیاز دارید مقادیر حساس (secret) را بهصورت امن در workflowها استفاده کنید 🔐، میتوانید از GitHub secrets استفاده کنید.
شما میتوانید secrets را در GitHub تعریف کنید و بدون اضافه کردن آنها به source control، در actionها از آنها استفاده کنید.
در workflow مربوط به deployment، من از secrets.AZURE_PUBLISH_PROFILE برای دسترسی به publish profile مربوط به App Service استفاده میکنم 🔑
جمعبندی 🧩
ءContinuous Integration و Continuous Delivery میتوانند فرآیند توسعه شما را متحول کنند ⚡️ و سرعت انتشار تغییرات را بهشدت افزایش دهند 🚀
سعی کنید محاسبه کنید چقدر زمان صرف deployment میکنید ⏱️
تقریباً مطمئنم از میزان زمانی که میتوانید با خودکارسازی ذخیره کنید شگفتزده خواهید شد 😮
نکته خوب اینجاست که معمولاً pipelineهای build و deployment را یکبار راهاندازی میکنید ✅ و سپس در تمام طول عمر پروژه از مزایای آنها استفاده میکنید ♻️
ممنون که خوندید 🙏
امیدوارم مفید بوده باشه ✨
اگر هر مشکلی را برای تیمت حل کنی، در واقع رهبر نیستی. داری جلوی رشدشان را میگیری. 🚧🧠
حتی اگر در ظاهر اینطور به نظر نرسد.
این وسوسه کاملاً طبیعی است، مخصوصاً وقتی باتجربهای.
• مشکل را میبینی 👀
• راهحل را میدانی ✅
• و دلت میخواهد کمک کنی 🤝
اما اگر هر بار خودت وارد عمل شوی، این اتفاقها میافتد:
1️⃣ تیم به تو وابسته میشود
2️⃣ مهارت حل مسئله در آنها رشد نمیکند
3️⃣ بدون اینکه بفهمی، خودت تبدیل به گلوگاه سیستم میشوی ⛔️
بهجای آن، این رویکرد را امتحان کن 👇
راهنمایی بده، نه جواب آماده 🧭
بپرس: «به نظرت خودمون باید چیکار کنیم؟» 🤔
اجازه بده افراد (در حد معقول) تقلا کنند تا رشد کنند 💪
کانتکست و دید کلی بده، نه فقط تصمیم نهایی یا اقدام آماده 📚
حتی اگر در ظاهر اینطور به نظر نرسد.
این وسوسه کاملاً طبیعی است، مخصوصاً وقتی باتجربهای.
• مشکل را میبینی 👀
• راهحل را میدانی ✅
• و دلت میخواهد کمک کنی 🤝
اما اگر هر بار خودت وارد عمل شوی، این اتفاقها میافتد:
1️⃣ تیم به تو وابسته میشود
2️⃣ مهارت حل مسئله در آنها رشد نمیکند
3️⃣ بدون اینکه بفهمی، خودت تبدیل به گلوگاه سیستم میشوی ⛔️
بهجای آن، این رویکرد را امتحان کن 👇
راهنمایی بده، نه جواب آماده 🧭
بپرس: «به نظرت خودمون باید چیکار کنیم؟» 🤔
اجازه بده افراد (در حد معقول) تقلا کنند تا رشد کنند 💪
کانتکست و دید کلی بده، نه فقط تصمیم نهایی یا اقدام آماده 📚
ءServer-Sent Events در ASP.NET Core و NET 10. 📡🚀
بهروزرسانیهای Real-time دیگر یک قابلیت «خوب است داشته باشیم» نیستند.
بیشتر رابطهای کاربری مدرن انتظار دارند به نوعی جریان زندهای از داده را از سمت سرور دریافت کنند.
سالها در اکوسیستم NET. ، پاسخ پیشفرض برای این نیاز SignalR بوده است.
در حالی که SignalR فوقالعاده قدرتمند است، اما برای سناریوهای سادهتر، داشتن گزینههای دیگر هم بسیار مفید است ✨
با انتشار ASP.NET Core 10، بالاخره یک API بومی و سطحبالا برای Server-Sent Events (SSE) داریم 🎉
این قابلیت فاصله بین polling سادهی HTTP و WebSocketهای دوطرفه از طریق SignalR را پر میکند.
🤔 چرا SSE بهجای SignalR؟
ءSignalR یک ابزار بسیار قدرتمند است که WebSockets، Long Polling و SSE را بهصورت خودکار مدیریت میکند و یک کانال ارتباطی دوطرفه (Full-Duplex) فراهم میکند.
اما این قدرت، هزینههایی هم دارد:
• استفاده از یک پروتکل مشخص (Hubs)
• نیاز به کتابخانهی سمت کلاینت
• نیاز به Sticky Session یا Backplane (مثل Redis) برای مقیاسپذیری
ءSSE متفاوت است، چون:
➡️ یکطرفه (Unidirectional) است: مخصوص استریم داده از سرور به کلاینت
🌐 ءHTTP بومی است: فقط یک درخواست استاندارد HTTP با text/event-stream
🔄 ءReconnect خودکار دارد: مرورگرها بهصورت Native با API به نام EventSource اتصال مجدد را مدیریت میکنند
🪶 سبک و ساده است: بدون کتابخانههای سنگین سمت کلاینت یا منطق handshake پیچیده
✨ سادهترین Endpoint برای Server-Sent Events
زیبایی API جدید SSE در NET 10.، سادگی آن است.
میتوانید از Results.ServerSentEvents استفاده کنید تا یک جریان از رویدادها را از هر <IAsyncEnumerable<T برگردانید.
از آنجایی که IAsyncEnumerable نمایانگر یک جریان داده است که در طول زمان میرسد، سرور متوجه میشود که باید اتصال HTTP را باز نگه دارد، بهجای اینکه بعد از اولین «chunk» آن را ببندد 🔓
در اینجا یک مثال مینیمال از یک Endpoint برای SSE وجود دارد که ثبت سفارشها را بهصورت Real-time استریم میکند 📦📊
app.MapGet("orders/realtime", (
ChannelReader<OrderPlacement> channelReader,
CancellationToken cancellationToken) =>
{
// 1. ReadAllAsync یک IAsyncEnumerable برمیگرداند
// 2. Results.ServerSentEvents به مرورگر میگوید: «این اتصال را باز نگه دار»
// 3. به محض ورود دادهی جدید به Channel، داده به کلاینت Push میشود
return Results.ServerSentEvents(
channelReader.ReadAllAsync(cancellationToken),
eventType: "orders");
});🔍 وقتی کلاینت این Endpoint را صدا میزند چه اتفاقی میافتد؟
🔹️سرور هدر Content-Type: text/event-stream را ارسال میکند 📬
🔸️اتصال باز میماند و در حالت انتظار برای داده قرار میگیرد ⏳
🔹️به محض اینکه اپلیکیشن شما یک سفارش جدید داخل Channel قرار دهد:
🔸️ءIAsyncEnumerable آن آیتم را yield میکند
🔹️ءNET. بلافاصله آن را از طریق همان اتصال HTTP باز به مرورگر ارسال میکند ⚡️
این یک روش فوقالعاده بهینه برای پیادهسازی اعلانهای Push است، بدون سربار یک پروتکل stateful.
🧠 نکتهی پایانی
در این مثال، از Channel فقط بهعنوان یک ابزار استفاده شده است.
در یک اپلیکیشن واقعی، ممکن است:
یک Background Service داشته باشید 🛠
به یک صف پیام مثل RabbitMQ یا Azure Service Bus گوش دهید 📮
یا به تغییرات دیتابیس واکنش نشان دهید 🗄
و سپس رویدادهای جدید را داخل Channel قرار دهید تا کلاینتهای متصل آنها را مصرف کنند.
مدیریت رویدادهای ازدسترفته (Handling Missed Events) 🔄📡
ءEndpoint سادهای که همین الان ساختیم عالی است، اما یک ضعف مهم دارد:
تابآوری (Resilience) ندارد.
یکی از بزرگترین چالشها در استریمهای Real-time، قطع شدن اتصال است.
تا زمانی که مرورگر بهصورت خودکار دوباره وصل شود، ممکن است چندین رویداد ارسال شده و از دست رفته باشند 😕
برای حل این مشکل، SSE یک مکانیزم داخلی دارد:
هدر Last-Event-ID.
وقتی مرورگر reconnect میشود، این ID را دوباره برای سرور ارسال میکند.
در NET 10. میتوانیم از نوع <SseItem<T استفاده کنیم تا داده را بههمراه متادیتاهایی مثل ID و retry interval بستهبندی کنیم.
با ترکیب یک OrderEventBuffer سادهی درونحافظهای و مقدار Last-Event-ID که مرورگر ارسال میکند، میتوانیم رویدادهای ازدسترفته را هنگام reconnect دوباره ارسال کنیم 🔁
app.MapGet("orders/realtime/with-replays", (
ChannelReader<OrderPlacement> channelReader,
OrderEventBuffer eventBuffer,
[FromHeader(Name = "Last-Event-ID")] string? lastEventId,
CancellationToken cancellationToken) =>
{
async IAsyncEnumerable<SseItem<OrderPlacement>> StreamEvents()
{
// 1. بازپخش رویدادهای ازدسترفته از buffer
if (!string.IsNullOrWhiteSpace(lastEventId))
{
var missedEvents = eventBuffer.GetEventsAfter(lastEventId);
foreach (var missedEvent in missedEvents)
{
yield return missedEvent;
}
}
// 2. استریم رویدادهای جدید بهمحض ورود به Channel
await foreach (var order in channelReader.ReadAllAsync(cancellationToken))
{
var sseItem = eventBuffer.Add(order); // Buffer یک ID یکتا اختصاص میدهد
yield return sseItem;
}
}
return TypedResults.ServerSentEvents(StreamEvents(), "orders");
});فیلتر کردن Server-Sent Events بر اساس کاربر 👤🔐
ءSSE روی HTTP استاندارد ساخته شده است.
چون یک درخواست GET معمولی است، زیرساخت فعلی شما بدون تغییر کار میکند:
ءSecurity 🔐: میتوانید JWT را بهصورت عادی در هدر Authorization ارسال کنید
ءUser Context 👤: میتوانید به HttpContext.User دسترسی داشته باشید و استریم را بر اساس UserId فیلتر کنید
→ فقط دادههایی که متعلق به همان کاربر هستند ارسال میشوند
مثال یک Endpoint SSE که فقط سفارشهای کاربر لاگینشده را استریم میکند:
app.MapGet("orders/realtime", (
ChannelReader<OrderPlacement> channelReader,
IUserContext userContext, // کانتکست تزریقشده شامل اطلاعات کاربر
CancellationToken cancellationToken) =>
{
// UserId از JWT توسط IUserContext استخراج میشود
var currentUserId = userContext.UserId;
async IAsyncEnumerable<OrderPlacement> GetUserOrders()
{
await foreach (var order in channelReader.ReadAllAsync(cancellationToken))
{
// فقط دادههای متعلق به کاربر احراز هویتشده ارسال میشود
if (order.CustomerId == currentUserId)
{
yield return order;
}
}
}
return Results.ServerSentEvents(GetUserOrders(), "orders");
})
.RequireAuthorization(); // Authorization استاندارد ASP.NET Core⚠️ نکته مهم:
وقتی یک پیام داخل Channel نوشته میشود، به تمام کلاینتهای متصل broadcast میشود.
این رفتار برای استریمهای per-user ایدهآل نیست.
در محیط production، احتمالاً به راهکار قویتری نیاز دارید.
مصرف Server-Sent Events در JavaScript 🌐🧠
در سمت کلاینت، نیازی به نصب حتی یک پکیج npm هم ندارید 🙌 API بومی مرورگر یعنی EventSource تمام کارهای سنگین را انجام میدهد،
از جمله reconnect خودکار و ارسال Last-Event-ID.
const eventSource = new EventSource('/orders/realtime/with-replays');
// گوش دادن به event type مشخصشده در C#
eventSource.addEventListener('orders', (event) => {
const payload = JSON.parse(event.data);
console.log(New Order ${event.lastEventId}:, payload.data);
});
// وقتی اتصال برقرار میشود
eventSource.onopen = () => {
console.log('Connection opened');
};
// پیامهای عمومی (در صورت وجود)
eventSource.onmessage = (event) => {
console.log('Received message:', event);
};
// مدیریت خطا و reconnect
eventSource.onerror = () => {
if (eventSource.readyState === EventSource.CONNECTING) {
console.log('Reconnecting...');
}
};جمعبندی 🧩✨
ءSSE در NET 10. یک نقطهی تعادل عالی است برای بهروزرسانیهای ساده و یکطرفه مثل:
داشبوردها 📊
نوتیفیکیشنها 🔔
ءProgress barها ⏳
سبک است، مبتنی بر HTTP است و بهراحتی با middlewareهای امنیتی فعلی شما ایمن میشود.
با این حال، SignalR همچنان انتخاب قدرتمند و battle-tested برای ارتباطهای دوطرفهی پیچیده یا مقیاس بسیار بالا (با backplane) باقی میماند.
هدف جایگزینی SignalR نیست ❌
هدف این است که برای کارهای ساده، ابزار سادهتری داشته باشید 🛠
سبکترین ابزاری را انتخاب کنید که مشکل شما را حل میکند.
امیدوارم مفید بوده باشد.😊
ءSSE در NET 10. یک نقطهی تعادل عالی است برای بهروزرسانیهای ساده و یکطرفه مثل:
داشبوردها 📊
نوتیفیکیشنها 🔔
ءProgress barها ⏳
سبک است، مبتنی بر HTTP است و بهراحتی با middlewareهای امنیتی فعلی شما ایمن میشود.
با این حال، SignalR همچنان انتخاب قدرتمند و battle-tested برای ارتباطهای دوطرفهی پیچیده یا مقیاس بسیار بالا (با backplane) باقی میماند.
هدف جایگزینی SignalR نیست ❌
هدف این است که برای کارهای ساده، ابزار سادهتری داشته باشید 🛠
سبکترین ابزاری را انتخاب کنید که مشکل شما را حل میکند.
امیدوارم مفید بوده باشد.😊
🧭 ۵ نکته برای گم نشدن وقتی وارد یک کدبیس موجود (و طاقتفرسا) میشی
ورود به یک کدبیس جدید میتونه مثل پریدن توی قسمت عمیق استخر باشه
(یا برای یک مهندس جونیور… وسط اقیانوس 🌊).
ممکنه ترسناک و بدون جهت به نظر برسه، اما این نکتهها کمک میکنن راهتو پیدا کنی 👇
1️⃣ خردش کن (Break It Down)
ما بهعنوان مهندس نرمافزار باید روی شکستن مسائل پیچیده تمرکز کنیم.
مشکلها رو به بخشهای کوچیکتر و قابلمدیریتتر تقسیم کن.
سعی نکن همهچیز رو یکجا بفهمی.
روی بخشی که الان روش کار میکنی تمرکز کن و کمکم دانشت رو گسترش بده.
وقتی مسئله بهاندازه کافی تجزیه بشه، با تسکهای خیلی سادهتری روبهرو میشی 🧩
2️⃣ سؤال بپرس — زیاد هم بپرس! ❓
یادت باشه هیچ سؤالی کوچیک یا بیاهمیت نیست.
بعضی وقتها آدمها در پرسیدن سؤال مردد میشن چون میترسن بیتجربه به نظر بیان.
اما واقعیت اینه که هر سؤال یک پله به سمت حرفهای شدنه.
تیمت خط نجات توئه — از تجربهشون استفاده کن 🛟
3️⃣ مسیر یادگیریت رو مستند کن 📝
برای خودت یک لاگ درست کن.
نقاشی بکش.
یادگیریها و چالشهات رو بنویس.
این کار هم کمک میکنه پیشرفتت رو ببینی، هم میتونه بعداً به یک منبع ارزشمند برای جونیورهای بعدی تبدیل بشه که جای تو قرار میگیرن.
اگه تیمت داکیومنتیشن داره، اگه دیدی قدیمی شده، با چیزهایی که یاد گرفتی آپدیتش کن 📚
4️⃣ ءPair Programming رو بپذیر 🤝
کار کردن دونفره با یک همتیمی باتجربه میتونه بازی رو عوض کنه.
بهترین پراکتیسها، میانبُرها و تلههایی که باید ازشون دوری کنی رو یاد میگیری.
فقط سعی کن خودت پشت فرمون بشینی 🚗
طوری که تو مسیریابی رو انجام بدی، نه اینکه فقط تماشاچی باشی.
5️⃣ این یک ماراتنه، نه دوی سرعت 🏃♂️
رم یکشبه ساخته نشد،
و درک کامل یک کدبیس هم همینطور.
با خودت صبور باش.
هر روز داری یک تکه از پازل رو اضافه میکنی 🧠🧩
برای همه مهندسهای جونیور 👶👩💻👨💻
یادتون باشه: هر متخصصی یک روزی مبتدی بوده.
مسئلهها رو به بخشهای قابلهضمتر تقسیم کن.
تکهتکه جلو برو 🔹🔹🔹
🚀 چگونه بدون استفاده از کتابخانههای خارجی یک Cache با کارایی بالا بسازیم
چند روز پیش داشتم به یک تکه کد نگاه میکردم که بیش از حد لازم کار انجام میداد 🤯
مطمئنم شما هم میتونید نمونهای مشابه ازش رو در اپلیکیشنهای خودتون پیدا کنید.
شاید یک فراخوانی دیتابیس باشه که باید سریعتر باشه 🐌،
یا یک API خارجی که کمکم داره برای هر درخواست ازتون هزینههای سنگین میگیره 💸.
اولین واکنش من معمولاً اینه:
👉 «خب، فقط cacheش میکنم.»
در NET.، این معمولاً یعنی استفاده از IMemoryCache
یا اضافه کردن یک distributed cache مثل Redis 🧠.
اما تا حالا شده از خودتون بپرسید واقعاً داخل این کتابخانهها چه اتفاقی میافته؟ 🤔
چرا برای ذخیرهی یک مقدار ساده در حافظه به اینهمه پیچیدگی نیاز داریم؟
برای همین، یک بعدازظهر رو صرف این کردم که یک cache با کارایی بالا رو از صفر بسازم ⏳⚙️.
من توصیه نمیکنم برای استفاده در محیط production خودتون یک کتابخانهی cache بنویسید ❌
اما من با انجام دادن یاد میگیرم ✍️.
درک این الگوها (concurrency، race condition و locking)
همون چیزیه که یک «coder» 👨💻 رو از یک مهندس نرمافزار واقعی 🧑🔧 جدا میکنه.
🧭 نقطهی شروع
من روی یک handler ساده برای تبدیل واحد ارز کار میکردم 💱.
ما برای گرفتن نرخ تبدیل ارزها از یک API شخص ثالث استفاده میکنیم 🌐.
این API نرخ تبدیل فعلی یک کد ارز مشخص
(مثل EUR، GBP، JPY) رو نسبت به USD برمیگردونه.
این پیادهسازی اولیه است 👇:
public static class CurrencyConversion
{
public static async Task<IResult> Handle(
string currencyCode,
decimal amount,
CurrencyApiClient currencyClient)
{
// Validate currency code format (3 uppercase letters)
if (string.IsNullOrWhiteSpace(currencyCode)||
currencyCode.Length != 3 ||
!currencyCode.All(char.IsLetter))
{
return Results.BadRequest(
new { error = "Currency code must be a 3-letter uppercase code (e.g., EUR, GBP)" });
}
// Validate amount (must be positive)
if (amount < 0)
{
return Results.BadRequest(new { error = "Amount must be a positive number" });
}
var rate = await currencyClient.GetExchangeRateAsync(currencyCode);
if (rate == null)
{
return Results.NotFound(
new { error = $"Exchange rate for {currencyCode} not found or API error occurred" });
}
var convertedAmount = amount * rate.Value;
return Results.Ok(new ExchangeRateResponse(
Currency: currencyCode,
BaseCurrency: "USD",
Rate: rate.Value,
Amount: amount,
ConvertedAmount: convertedAmount
));
}
}
این کد در محیط توسعهی لوکال کاملاً خوب کار میکنه ✅
اما در محیط production، اگه ۱۰۰ نفر همزمان به این endpoint درخواست بزنن 😬،
شما دارید ۱۰۰ فراخوانی شبکهی کاملاً یکسان انجام میدید 📡📡📡.
ارائهدهندهی API از شما متنفر میشه 😡
(و احتمالاً rate limit هم میشید 🚫)
و latency سیستمتون بهشدت افزایش پیدا میکنه 📈.
حالا بیاید بدون استفاده از هیچ کتابخانهی خارجی،
یک cache بسازیم تا این مشکل رو حل کنیم 🛠🧠.
یادتون باشه، این کار فقط برای یادگیری انجام میشه 🎓.
🧱 سطح 1️⃣ : اضافه کردن ConcurrentDictionary
اولین فکری که احتمالاً به ذهنت میرسد این است که نرخها را داخل یک ConcurrentDictionary ذخیره کنی 🧠.
این ساختار thread-safe است، پس در نگاه اول ابزار درستی به نظر میرسد ✅.
private static readonly ConcurrentDictionary<string, decimal> Cache = new();
// In the Handler:
if (Cache.TryGetValue(currencyCode, out var cachedRate))
{
return cachedRate;
}
var rate = await currencyClient.GetExchangeRateAsync(currencyCode);
Cache.TryAdd(currencyCode, rate.Value);
این کار قطعاً تحت بار بالا به بهبود performance کمک میکند 🚀.
چندین thread میتوانند همزمان از دیکشنری بخوانند یا در آن بنویسند بدون اینکه برنامه کرش کند 💪.
اما ConcurrentDictionary فقط از ساختار دیکشنری محافظت میکند، نه از منطق شما ⚠️.
اگر ۱۰۰ کاربر دقیقاً در یک لحظه نرخ "EUR" را درخواست کنند 👥👥👥، TryGetValue برای همهی آنها false برمیگرداند.
در نتیجه، همهشان همزمان API را صدا میزنند 📡📡📡.
این یک race condition کلاسیک است 🏁.
شما حافظه را امن کردهاید، اما از API خارجی محافظت نکردهاید ❌.
یک مشکل دیگر هم وجود دارد:
نرخها هیچوقت expire نمیشوند ⏳❌.
⏰ سطح 2️⃣: اضافه کردن انقضای Cache
نرخ ارزها برای همیشه ثابت نمیمانند 💱.
ما به راهی نیاز داریم که بعد از مدتی آنها را منقضی کنیم.
از آنجایی که ConcurrentDictionary مفهوم Time To Live (TTL) ندارد،
باید دادههایمان را wrap کنیم 📦.
// Store both the rate and the time it was created
private record CacheEntry(decimal Rate, DateTime CreatedAt);
// Our cache now stores CacheEntry objects
private static readonly ConcurrentDictionary<string, CacheEntry> Cache = new();
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);
// Check: Is it there? And is it still "fresh"?
if (Cache.TryGetValue(currencyCode, out var entry) &&
(DateTime.UtcNow - entry.CreatedAt) < CacheDuration)
{
return entry.Rate;
}
حالا expiration داریم ✅.
اما در عوض، یک مشکل جدید ساختهایم 😬:
🐘 Thundering Herd (یا Cache Stampede)
هر ۵ دقیقه یکبار، وقتی cache منقضی میشود ⏱️،
تمام درخواستهای ورودی بهصورت همزمان دادهی «منقضیشده» میبینند
و همگی تلاش میکنند آن را refresh کنند 💥.
پس باید این مشکل را در مرحلهی بعدی حل کنیم.
🚦 سطح 3️⃣: حل مشکل «Cache Stampede»
برای حل این مشکل، باید مطمئن شویم که
فقط یک نفر اجازه دارد داده را بهروزرسانی کند
و بقیه منتظر بمانند ⏸️.
در #C چطور این کار را انجام میدهیم؟ 🤔
ما از SemaphoreSlim و الگویی به نام Double-Checked Locking استفاده میکنیم 🔐.
اول یک بار cache را چک میکنیم (مسیر سریع 🏃♂️)،
بعد lock میگیریم،
و سپس دوباره چک میکنیم تا ببینیم آیا در این فاصله thread دیگری cache را پر کرده یا نه.
// Basically a mutex but async-friendly
private static readonly SemaphoreSlim Lock = new(1, 1);
public static async Task<decimal> GetRateAsync(string code, CurrencyApiClient client)
{
// Fast path: No locking needed
if (Cache.TryGetValue(code, out var entry) && IsFresh(entry))
{
return entry.Rate;
}
var acquired = await Lock.WaitAsync(TimeSpan.FromSeconds(10)); // Avoid deadlocks
if (!acquired)
{
throw new Exception("Could not acquire lock to fetch exchange rate.");
}
try
{
// Double-check: Did someone else finish the API call while we waited?
if (Cache.TryGetValue(code, out entry) && IsFresh(entry))
{
return entry.Rate;
}
var rate = await client.GetExchangeRateAsync(code);
var newEntry = new CacheEntry(rate.Value, DateTime.UtcNow);
// Atomically update the cache
// This is safe because we're inside the lock
Cache.AddOrUpdate(code, newEntry, (_, _) => newEntry);
return rate.Value;
}
finally
{
// Always release the lock
Lock.Release();
}
}
این کار یک بهبود واقعی است 👍.
اما هنوز یک حس بد وجود دارد… 😐
میتوانی مشکل این کد را پیدا کنی؟ 👀
🔒 این lock مثل یک global lock رفتار میکند.
یعنی اگر یک thread در حال گرفتن نرخ "EUR" باشد،
تمام threadهای دیگر (حتی آنهایی که "JPY" میخواهند 🇯🇵)
تا پایان درخواست "EUR" بلاک میشوند ⛔️.
این مشکل به lock contention معروف است ⚠️.
در مرحلهی بعدی، این مشکل را هم حل میکنیم 🛠✨.
🚀 سطح 4️⃣: مقیاسپذیری با Keyed Locking
حرکت «حرفهای» 👌 در اینجا استفاده از Keyed Locking است 🔑.
ما برای هر ارز مشخص یک lock جداگانه ایجاد میکنیم. از آنجایی که تعداد ارزها محدود است 💱، این روش از نظر مصرف حافظه سنگین نیست.
ما به یک ConcurrentDictionary اضافه نیاز داریم تا semaphoreها را بهازای هر کد ارز نگهداری کنیم 🧵.
private static readonly ConcurrentDictionary<string, SemaphoreSlim> Locks = new();
// In the Handler:
var semaphore = Locks.GetOrAdd(currencyCode, _ => new SemaphoreSlim(1, 1));
if (!Cache.TryGetValue(currencyCode, out var cachedRate) &&
DateTime.UtcNow - cachedRate?.CreatedAt < CacheDuration)
{
var acquired = await semaphore.WaitAsync(TimeSpan.FromSeconds(10));
if (!acquired)
{
throw new Exception("Could not acquire lock to fetch exchange rate.");
}
try
{
// Fetch and update logic...
}
finally { semaphore.Release(); }
}
تنها چیزی که تغییر کرده، نحوهی گرفتن lock است 🔒.
حالا اگر یک thread در حال گرفتن نرخ "EUR" باشد 🇪🇺، threadهای دیگر که "JPY" را درخواست کردهاند 🇯🇵، بدون معطلی ادامه میدهند ⏩.
این مقیاسپذیرترین نسخه از cache ماست 📈.
اما… ⚠️
این راهحل فقط در حافظه کار میکند.
پس برای سیستمهای توزیعشده یا چندین instance سرور مناسب نیست 🌐.
همچنین چند edge case دیگر هم وجود دارد که میتوانی آنها را بهعنوان تمرین بررسی کنی 🧠.
🧩 کد نهایی
در اینجا نسخهی نهایی منطق cache را میبینی:
public static class CurrencyConversion
{
private record CacheEntry(decimal Rate, DateTime CreatedAt);
private static readonly ConcurrentDictionary<string, CacheEntry> Cache = new();
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);
private static readonly ConcurrentDictionary<string, SemaphoreSlim> Locks = new();
public static async Task<IResult> Handle(
string currencyCode,
decimal amount,
CurrencyApiClient currencyClient)
{
// Validate currency code format (3 uppercase letters)
if (string.IsNullOrWhiteSpace(currencyCode)||
currencyCode.Length != 3 ||
!currencyCode.All(char.IsLetter))
{
return Results.BadRequest(
new { error = "Currency code must be a 3-letter uppercase code (e.g., EUR, GBP)" });
}
// Validate amount (must be positive)
if (amount < 0)
{
return Results.BadRequest(new { error = "Amount must be a positive number" });
}
decimal? rate;
var semaphore = Locks.GetOrAdd(currencyCode, _ => new SemaphoreSlim(1, 1));
if (!Cache.TryGetValue(currencyCode, out var cachedRate) &&
DateTime.UtcNow - cachedRate?.CreatedAt < CacheDuration)
{
var acquired = await semaphore.WaitAsync(TimeSpan.FromSeconds(10));
if (!acquired)
{
throw new Exception("Could not acquire lock to fetch exchange rate.");
}
try
{
// Double-check locking pattern: check again inside the lock
if (!Cache.TryGetValue(currencyCode, out cachedRate) &&
DateTime.UtcNow - cachedRate?.CreatedAt < CacheDuration)
{
rate = await currencyClient.GetExchangeRateAsync(currencyCode);
if (rate == null)
{
return Results.NotFound(
new { error = $"Exchange rate for {currencyCode} not found or API error occurred" });
}
Cache.AddOrUpdate(currencyCode,
_ => new CacheEntry(rate.Value, DateTime.UtcNow),
(_, _) => new CacheEntry(rate.Value, DateTime.UtcNow));
}
else
{
rate = cachedRate!.Rate;
}
}
finally
{
semaphore.Release();
}
}
else
{
rate = cachedRate!.Rate;
}
var convertedAmount = amount * rate.Value;
return Results.Ok(new ExchangeRateResponse(
Currency: currencyCode,
BaseCurrency: "USD",
Rate: rate.Value,
Amount: amount,
ConvertedAmount: convertedAmount
));
}
}
گام بعدی این است که منطق اصلی cache را به یک کلاس reusable جداگانه استخراج کنی 🧩.
به این شکل میتوانی آن را در بخشهای دیگر برنامه هم استفاده کنی 🔁.
🎯 جمعبندی (Takeaway)
چرا اصلاً این همه دردسر؟ 🤔
دیدن یک ConcurrentDictionary ساده خیلی وسوسهبرانگیز است 😌 و فکر میکنی کار تمام شده.
اما همانطور که دیدیم، فاصلهی بین «کار میکند» و «مقیاسپذیر است»
پر از edge caseهایی است که میتوانند یک سیستم production را زمینگیر کنند 💥.
وقتی از یک library استفاده میکنی، این edge caseها برایت مدیریت میشوند 🛡.
اما ساختن آن با دست خودت، به تو سه ستون اصلی کد با کارایی بالا را یاد میدهد 🏛:
🧵 Thread safety
🔒 Lock contention
🛡 Resource protection
گاهی اوقات، «کسلکنندهترین» بخشهای زیرساخت، مثل cache،
در واقع جالبترین بخشها از نظر معماری هستند 🧠✨.
و آن ۱٪ مواقعی هم هست که به یک راهحل کاملاً سفارشی نیاز داری
که هیچ libraryای ارائهاش نمیدهد 🛠.
برای همین دانستن اصول پایه واقعاً ارزشمند است.
کتابخانههای مدرنی مثل HybridCache یا FusionCache این کارها را برایت انجام میدهند 🚀،
اما درک این الگوها باعث میشود دقیقاً بدانی چرا برنامهات تحت load آنطور که میبینی رفتار میکند 📊.