In-memory Cache — IMemoryCache, sliding/absolute expiration

~20 daq · .NET Backend · Ilg'or ASP.NET Core

Muammodan boshlaylik

Bosh sahifada "Kategoriyalar" ro'yxati. Har bir so'rovda — bazaga so'rov, JOIN, hisoblash. Kategoriyalar oyiga bir marta o'zgaradi, lekin daqiqasiga 3000 marta o'qiladi. Baza bir xil ishni 3000 marta qiladi; kampaniya kunida u bo'g'iladi va butun sayt sekinlashadi.


Hayotiy o'xshatma

Ofitsiant va menyu. Har bir mehmon "bugun nima bor?" deb so'raganda, ofitsiant oshxonaga borib oshpazdan so'ramaydi — ertalab bir marta so'rab, yozib olgan. Kun davomida yozuvdan o'qiydi. Biror taom tugasa — oshpaz aytadi, ofitsiant yozuvni yangilaydi.


Cache nima?

Cache — tez-tez so'raladigan va kam o'zgaradigan ma'lumotni tezkor joyda (xotirada) vaqtincha saqlash.

Manba Taxminiy vaqt
Xotira (IMemoryCache) mikrosekundlar
Redis (tarmoq orqali) ~1 ms
Baza so'rovi 5–100+ ms
Tashqi API 100–1000+ ms

IMemoryCache

builder.Services.AddMemoryCache();
public class KategoriyaService(DokonDbContext db, IMemoryCache cache)
{
    private const string Kalit = "kategoriyalar:hammasi";

    public async Task<List<KategoriyaDto>> HammasiAsync(CancellationToken ct)
    {
        return (await cache.GetOrCreateAsync(Kalit, async entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30);
            return await db.Kategoriyalar
                .AsNoTracking()
                .OrderBy(k => k.Nomi)
                .Select(k => new KategoriyaDto(k.Id, k.Nomi))
                .ToListAsync(ct);
        }))!;
    }
}

Birinchi so'rov — bazadan olib cache ga yozadi; keyingi 30 daqiqa — xotiradan.

Qo'lda boshqarish

if (!cache.TryGetValue(Kalit, out List<KategoriyaDto>? natija))
{
    natija = await BazadanOlAsync(ct);
    cache.Set(Kalit, natija, TimeSpan.FromMinutes(30));
}

Muddat turlari

var opt = new MemoryCacheEntryOptions()
    .SetAbsoluteExpiration(TimeSpan.FromHours(1))     // 1 soatdan keyin — albatta o'chadi
    .SetSlidingExpiration(TimeSpan.FromMinutes(10))   // 10 daqiqa so'ralmasa — o'chadi
    .SetPriority(CacheItemPriority.High)
    .SetSize(1);
Absolute Sliding
Ma'nosi Belgilangan vaqtdan keyin Oxirgi murojaatdan keyin
Xavf — Tez-tez so'ralsa — hech qachon yangilanmaydi

[!TIP] Sliding ishlatsangiz — doim absolute bilan birga. Faqat sliding bo'lsa, mashhur kalit abadiy cache da qoladi va eskirgan ma'lumot ko'rsatiladi.


Invalidatsiya — cache ni tozalash

"Kompyuter fanida ikki qiyin narsa bor: cache invalidatsiyasi va narsalarga nom berish." — Phil Karlton

Ma'lumot o'zgarganda — cache ni o'chiring:

public async Task YaratAsync(KategoriyaYaratDto dto, CancellationToken ct)
{
    db.Kategoriyalar.Add(new Kategoriya { Nomi = dto.Nomi });
    await db.SaveChangesAsync(ct);
    cache.Remove(Kalit);           // keyingi o'qish bazadan yangisini oladi
}

Strategiyalar:

  • Muddat bo'yicha — biroz eskirgan ma'lumotga chidash mumkin bo'lsa (statistika, tavsiyalar)
  • O'zgarishda o'chirish — aniqlik muhim bo'lsa (narx, qoldiq)
  • Ikkalasi birga — eng ishonchli

Kalitlarni nomlash

quot;mahsulot:{id}"
quot;mahsulotlar:kategoriya:{katId}:sahifa:{sahifa}"
quot;foydalanuvchi:{userId}:savat"

[!WARNING] Foydalanuvchiga xos ma'lumotni kalitda foydalanuvchi ID siz saqlamang — "savat" kaliti bilan birinchi foydalanuvchining savati hammaga ko'rinadi. Shaxsiy ma'lumot uchun kalitda doim egasi bo'lsin.


Xotira chegarasi

builder.Services.AddMemoryCache(o => o.SizeLimit = 10_000);

SizeLimit qo'yilsa — har bir yozuvda SetSize majburiy; chegara to'lganda past prioritetlilar chiqariladi. Chegarasiz cache — xotira cheksiz o'sishi mumkin.


HybridCache (.NET 9+)

builder.Services.AddHybridCache();

public class KategoriyaService(HybridCache cache, DokonDbContext db)
{
    public async Task<List<KategoriyaDto>> HammasiAsync(CancellationToken ct) =>
        await cache.GetOrCreateAsync("kategoriyalar:hammasi",
            async token => await db.Kategoriyalar.Select(k => new KategoriyaDto(k.Id, k.Nomi)).ToListAsync(token),
            cancellationToken: ct);
}

Afzalliklari:

  • Stampede himoyasi — cache bo'sh paytda 1000 ta so'rov kelsa, bazaga faqat bitta so'rov ketadi, qolganlari natijani kutadi
  • Ikki darajali: xotira (L1) + Redis (L2, 68-dars) — bitta API bilan
  • Teglar bo'yicha o'chirish (RemoveByTagAsync)

IMemoryCache cheklovi

Har bir server nusxasining o'z xotirasi bor. 3 ta server bo'lsa — 3 ta alohida cache: birida o'chirilgan qiymat boshqasida qoladi. Bir nechta server uchun — taqsimlangan cache (Redis, 68-dars).


Tez-tez uchraydigan xatolar

Tracked entity ni cache lash — DbContext bilan bog'liq ob'ekt; DTO yoki AsNoTracking natijasini saqlang.

Cache dagi ob'ektni o'zgartirish — xotiradagi ob'ekt bitta, o'zgarish hamma so'rovlarga ta'sir qiladi; o'zgarmas (record) turlar.

Hammasini cache lash — tez-tez o'zgaradigan yoki kam so'raladigan ma'lumot uchun foyda yo'q, faqat xato xavfi.


Xulosa

Cache — tez-tez o'qiladigan, kam o'zgaradigan ma'lumotni xotirada saqlash. AddMemoryCache + IMemoryCache: GetOrCreateAsync, TryGetValue/Set, Remove. Muddat: absolute (aniq vaqt) va sliding (so'ralmasa) — sliding faqat absolute bilan birga.

Invalidatsiya — muddat va o'zgarishda Remove. Kalitlar — tartibli, shaxsiy ma'lumotda egasi bilan. SizeLimit bilan xotirani cheklang. .NET 9+ HybridCache — stampede himoyasi va L1+L2. IMemoryCache — faqat bitta server uchun.


Keyingi dars

68-dars: Redis Cache — IDistributedCache, StackExchange.Redis