یکی از سریعترین و کمهزینهترین راهها برای بهبود چشمگیر کارایی یک سیستم، کشینگ (Caching) است. با این حال، همان جملهی معروف در دنیای برنامهنویسی را حتماً شنیدهاید: «تنها دو چیز سخت در علوم کامپیوتر وجود دارد: Cache Invalidation و نامگذاری». در این مقاله یاد میگیریم کشینگ دقیقاً چیست، چه الگوهایی دارد، و چگونه آن را در ASP.NET Core بهدرستی پیادهسازی کنیم.
کشینگ چیست؟
کشینگ یعنی نگهداشتن یک نسخه از دادهای که تولید یا واکشی آن هزینهبر است (زمانبر یا پرمصرف از نظر منابع)، در یک لایهی سریعتر، تا در درخواستهای بعدی نیازی به تولید دوبارهی آن نباشد. بهجای رفتن مکرر به دیتابیس یا صدا زدن یک API خارجی کند، نتیجه را یک بار در حافظه یا یک سیستم سریع ذخیره میکنیم و از همانجا سرو میکنیم.
قانون طلایی کشینگ: هر چیزی که تولیدش گران است ولی زیاد تغییر نمیکند، کاندید خوبی برای کش شدن است. نتیجهی یک کوئری سنگین، پاسخ یک API خارجی، یا خروجی یک محاسبهی پیچیده، نمونههای رایج هستند.
انواع کشینگ
کشینگ در چند سطح مختلف میتواند اتفاق بیفتد؛ هر سطح مزایا و محدودیتهای خودش را دارد:
مقایسهی 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 (منقضیشده) میبیند؛ اگر بیشازحد باطل کنید، مزیت کشینگ را از دست میدهید. رایجترین استراتژیها:
قانون عملی: برای دادههایی که تغییرشان قابل پیشبینی نیست (مثل قیمت محصول)، از 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)