معماری نرم‌افزار

همه‌چیز درباره‌ی Caching
از مفهوم تا پیاده‌سازی در ASP.NET Core

چرا کشینگ سریع‌ترین راه برای بهبود کارایی یک سیستم است، چه انواعی دارد، و چطور آن را درست و بدون سردرگمی در پروژه‌های دات‌نت پیاده‌سازی کنیم.

یکی از سریع‌ترین و کم‌هزینه‌ترین راه‌ها برای بهبود چشمگیر کارایی یک سیستم، کشینگ (Caching) است. با این حال، همان جمله‌ی معروف در دنیای برنامه‌نویسی را حتماً شنیده‌اید: «تنها دو چیز سخت در علوم کامپیوتر وجود دارد: Cache Invalidation و نام‌گذاری». در این مقاله یاد می‌گیریم کشینگ دقیقاً چیست، چه الگوهایی دارد، و چگونه آن را در ASP.NET Core به‌درستی پیاده‌سازی کنیم.


کشینگ چیست؟

کشینگ یعنی نگه‌داشتن یک نسخه از داده‌ای که تولید یا واکشی آن هزینه‌بر است (زمان‌بر یا پرمصرف از نظر منابع)، در یک لایه‌ی سریع‌تر، تا در درخواست‌های بعدی نیازی به تولید دوباره‌ی آن نباشد. به‌جای رفتن مکرر به دیتابیس یا صدا زدن یک API خارجی کند، نتیجه را یک بار در حافظه یا یک سیستم سریع ذخیره می‌کنیم و از همان‌جا سرو می‌کنیم.

قانون طلایی کشینگ: هر چیزی که تولیدش گران است ولی زیاد تغییر نمی‌کند، کاندید خوبی برای کش شدن است. نتیجه‌ی یک کوئری سنگین، پاسخ یک API خارجی، یا خروجی یک محاسبه‌ی پیچیده، نمونه‌های رایج هستند.


انواع کشینگ

کشینگ در چند سطح مختلف می‌تواند اتفاق بیفتد؛ هر سطح مزایا و محدودیت‌های خودش را دارد:

1
In-Memory Cache
داده در حافظه‌ی همان پروسه‌ی اپلیکیشن نگه‌داری می‌شود؛ سریع‌ترین نوع کش، اما محدود به یک نمونه (Instance) از سرور
2
Distributed Cache
داده در یک سرویس مجزا (مثل Redis) نگه‌داری می‌شود؛ بین چند نمونه از اپلیکیشن به‌اشتراک گذاشته می‌شود
3
Client-Side Cache
داده در مرورگر یا اپلیکیشن کلاینت نگه‌داری می‌شود؛ مثل کش HTTP یا LocalStorage
4
CDN / Edge Cache
محتوای استاتیک (تصاویر، CSS، JS) نزدیک به جغرافیای کاربر کش می‌شود

مقایسه‌ی In-Memory Cache و Distributed Cache

ویژگی In-Memory Cache Distributed Cache (Redis)
سرعتبسیار سریع (بدون شبکه)سریع، اما با تأخیر شبکه
اشتراک بین سرورهاخیربله
پایداری پس از ری‌استارتاز بین می‌رودمی‌تواند حفظ شود
مناسب برایتک‌سرور یا داده‌ی خاص یک نمونهمحیط مقیاس‌پذیر با چند نمونه (Scale-out)
مصرف حافظه سرور اپلیکیشنمستقیم درگیر می‌کندجدا از حافظه‌ی اپلیکیشن

الگوهای رایج کشینگ

۱. Cache-Aside (Lazy Loading)

رایج‌ترین الگو: ابتدا اپلیکیشن به کش سر می‌زند؛ اگر داده موجود بود (Cache Hit) همان را برمی‌گرداند، اگر نبود (Cache Miss) از منبع اصلی (دیتابیس) می‌خواند، در کش می‌گذارد و سپس نتیجه را برمی‌گرداند.

۲. Write-Through

هر بار که داده در دیتابیس نوشته می‌شود، همزمان کش هم به‌روزرسانی می‌شود. تضمین می‌کند کش همیشه هم‌گام با منبع اصلی است، اما هزینه‌ی نوشتن را افزایش می‌دهد.

۳. Write-Behind (Write-Back)

نوشتن ابتدا در کش انجام می‌شود و به‌صورت ناهم‌زمان (Async) به دیتابیس منتقل می‌شود. سرعت نوشتن را بالا می‌برد، اما ریسک از دست رفتن داده در صورت خرابی را دارد.

۴. Read-Through

مشابه Cache-Aside است، با این تفاوت که منطق واکشی از منبع اصلی داخل خود لایه‌ی کش قرار می‌گیرد، نه در کد اپلیکیشن.


چالش اصلی: Cache Invalidation

بزرگ‌ترین چالش کشینگ، ابطال به‌موقع داده‌ی قدیمی است. اگر کش را به‌درستی باطل نکنید، کاربر داده‌ی Stale (منقضی‌شده) می‌بیند؛ اگر بیش‌ازحد باطل کنید، مزیت کشینگ را از دست می‌دهید. رایج‌ترین استراتژی‌ها:

Time-based Expiration (TTL) — انقضای خودکار بعد از مدت زمان مشخص
Sliding Expiration — تمدید انقضا با هر بار استفاده
Event-based Invalidation — باطل کردن دستی هنگام تغییر داده
Cache Tagging / Dependency — گروه‌بندی کلیدها برای ابطال دسته‌جمعی

قانون عملی: برای داده‌هایی که تغییرشان قابل پیش‌بینی نیست (مثل قیمت محصول)، از TTL کوتاه + ابطال رویدادمحور به‌صورت ترکیبی استفاده کنید؛ نه فقط به یکی از این دو تکیه کنید.


پیاده‌سازی کشینگ در ASP.NET Core

دات‌نت چند ابزار built-in برای کشینگ فراهم کرده که هرکدام برای سناریوی خاصی مناسب‌اند:

  • IMemoryCache — برای کش در حافظه‌ی همان پروسه؛ ساده‌ترین راه برای شروع
  • IDistributedCache — یک abstraction یکسان برای کش توزیع‌شده؛ با پیاده‌سازی‌های Redis، SQL Server یا NCache
  • Output Caching (از NET 7 به بعد) — کش کردن کل پاسخ یک Endpoint، مستقیم روی سطح Middleware
  • Response Caching — کش کردن پاسخ HTTP بر اساس هدرهای استاندارد کش مرورگر

نمونه‌ی پیاده‌سازی الگوی Cache-Aside با IDistributedCache و Redis:

public async Task<ProductDto> GetProductAsync(int id)
{
    var cacheKey = $"product:{id}";
    var cached = await _cache.GetStringAsync(cacheKey);
 
    if (cached is not null)
        return JsonSerializer.Deserialize<ProductDto>(cached)!;
 
    var product = await _repository.GetByIdAsync(id);
 
    await _cache.SetStringAsync(
        cacheKey,
        JsonSerializer.Serialize(product),
        new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10),
            SlidingExpiration = TimeSpan.FromMinutes(3)
        });
 
    return product;
}

و ثبت Redis به‌عنوان Distributed Cache در Program.cs:

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
    options.InstanceName = "KodMemari_";
});

نکته‌ی مهم در طراحی لایه‌بندی‌شده (Onion / Clean Architecture): منطق کشینگ نباید مستقیم داخل Domain یا Application قرار بگیرد. بهترین‌ روش، استفاده از الگوی Decorator روی Repository یا Service است؛ به این شکل که یک کلاس Decorator، پیاده‌سازی اصلی را می‌پیچد و منطق کش را اضافه می‌کند، بدون این‌که Interface اصلی تغییر کند.


اشتباهات رایج در کشینگ

  • کش کردن بیش‌ازحد: کش کردن داده‌هایی که کمتر از هزینه‌ی خودِ کشینگ ارزش دارند
  • فراموش کردن Invalidation: نوشتن داده در دیتابیس بدون باطل کردن یا به‌روزرسانی کش مرتبط
  • Cache Stampede: وقتی کلید پرترافیکی هم‌زمان منقضی می‌شود و صدها درخواست هم‌زمان به دیتابیس هجوم می‌برند
  • کلید‌گذاری ضعیف: کلیدهای کش بدون namespace یا نسخه‌بندی مشخص، که باعث تداخل یا داده‌ی نادرست می‌شود
  • کش کردن داده‌ی حساس کاربر بدون در نظر گرفتن جداسازی صحیح بر اساس شناسه‌ی کاربر

برای مقابله با Cache Stampede، از تکنیک‌هایی مثل قفل توزیع‌شده (Distributed Lock) هنگام بازتولید کش، یا Probabilistic Early Expiration استفاده کنید تا همه‌ی درخواست‌ها هم‌زمان به منبع اصلی هجوم نبرند.


جمع‌بندی

کشینگ یکی از قدرتمندترین ابزارها برای بهبود کارایی است، اما مثل هر ابزار قدرتمند دیگری، اگر بدون برنامه استفاده شود می‌تواند باگ‌های عجیب و داده‌ی نادرست تولید کند. نکات کلیدی که باید همیشه به خاطر داشت:

  • فقط داده‌های گران و کم‌تغییر را کش کنید
  • همیشه از ابتدا استراتژی Invalidation را طراحی کنید، نه بعداً
  • برای محیط‌های Scale-out، از Distributed Cache استفاده کنید نه In-Memory
  • منطق کش را از منطق کسب‌وکار جدا نگه دارید (الگوی Decorator)

می‌خوای عمیق‌تر یاد بگیری؟

دوره‌های عملی معماری نرم‌افزار و دات‌نت رو در «کد و معماری» دنبال کن.

مشاهده مقالات