Caching — Cache Manager, Redis

~25 daq · NestJS · Fayllar va Xavfsizlik

Muammodan boshlaylik

GET /products — 500 ta mahsulot. Har so'rovda baza so'rovi, filtr, sort. Soniyada 1000 ta foydalanuvchi so'raydi. Baza qulab tushdi.

Lekin mahsulotlar har soniyada o'zgarmaydi — 5 daqiqada bir marta yangilanadi. Har so'rovda baza so'rovi qilish — isrof.

Caching: birinchi so'rovda natija hisoblanadi va saqlanadi. Keyingi so'rovlarda bazaga bormasdan saqlangan natija qaytariladi.


Loyiha tuzilishi

src/
├── app.module.ts        ← CacheModule ulash
│
├── products/
│   ├── products.module.ts
│   ├── products.controller.ts  ← @CacheKey, @CacheTTL
│   └── products.service.ts     ← CACHE_MANAGER inject
│
└── cache/
    └── cache.config.ts  ← Redis sozlamalari

O'rnatish

npm install @nestjs/cache-manager cache-manager
npm install cache-manager-redis-yet   # Redis uchun
npm install ioredis

Xotira cache (dev uchun)

// src/app.module.ts
import { CacheModule } from '@nestjs/cache-manager';

@Module({
  imports: [
    CacheModule.register({
      isGlobal: true,  // barcha modullarda ishlatish
      ttl:  300,       // standart: 5 daqiqa (soniyada)
      max:  1000,      // maksimal 1000 ta element
    }),
  ],
})
export class AppModule {}

Redis cache (production uchun)

// src/cache/cache.config.ts
import { redisStore } from 'cache-manager-redis-yet';
import { ConfigService } from '@nestjs/config';

export const cacheConfig = {
  isGlobal: true,
  store: async () => {
    const config = new ConfigService();
    return redisStore({
      socket: {
        host: config.get('REDIS_HOST', 'localhost'),
        port: config.get<number>('REDIS_PORT', 6379),
      },
      ttl: 300 * 1000, // millisekunda — 5 daqiqa
    });
  },
};
// src/app.module.ts
import { CacheModule } from '@nestjs/cache-manager';
import { cacheConfig } from './cache/cache.config';

@Module({
  imports: [
    CacheModule.registerAsync({
      isGlobal: true,
      useFactory: async (config: ConfigService) => ({
        store: await redisStore({
          socket: {
            host: config.get('REDIS_HOST'),
            port: config.get('REDIS_PORT'),
          },
          ttl: 300_000,
        }),
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}

1-usul: CACHE_MANAGER — qo'lda boshqarish

Eng moslashuvchan usul. Cache ni to'liq nazorat qilasiz:

// src/products/products.service.ts
import { Injectable } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Inject } from '@nestjs/common';
import { Cache } from 'cache-manager';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';

@Injectable()
export class ProductsService {
  constructor(
    @InjectModel('Product') private productModel: Model<any>,
    @Inject(CACHE_MANAGER) private cache: Cache, // ← inject
  ) {}

  async findAll(query: any) {
    // Cache kaliti — so'rov parametrlaridan
    const key = `products:${JSON.stringify(query)}`;

    // 1. Cacheda bormi?
    const cached = await this.cache.get(key);
    if (cached) return cached; // ← baza so'rovi yo'q

    // 2. Yo'q — bazadan olamiz
    const data = await this.productModel
      .find(query)
      .sort({ createdAt: -1 })
      .exec();

    // 3. Cachega yozamiz — 5 daqiqa
    await this.cache.set(key, data, 300_000);

    return data;
  }

  async findOne(id: string) {
    const key = `product:${id}`;

    const cached = await this.cache.get(key);
    if (cached) return cached;

    const product = await this.productModel.findById(id).exec();
    if (!product) throw new NotFoundException('Topilmadi');

    await this.cache.set(key, product, 600_000); // 10 daqiqa
    return product;
  }

  // Mahsulot o'zgarganda — cacheni tozalash
  async update(id: string, dto: any) {
    const product = await this.productModel
      .findByIdAndUpdate(id, dto, { new: true })
      .exec();

    // Eski cache yaroqsiz — o'chiramiz
    await this.cache.del(`product:${id}`);
    await this.invalidateListCache(); // ro'yxat ham yangilanishi kerak

    return product;
  }

  async remove(id: string) {
    await this.productModel.findByIdAndDelete(id).exec();
    await this.cache.del(`product:${id}`);
    await this.invalidateListCache();
    return { message: 'O\'chirildi' };
  }

  // Ro'yxat cache larini tozalash (pattern bo'yicha)
  private async invalidateListCache() {
    // Redis da pattern bilan o'chirish
    // Xotira cache da barcha kalitlarni olib, filtrlaymiz
    await this.cache.del('products:*'); // ← Redis pattern (store ga bog'liq)
  }
}

2-usul: @CacheKey va @CacheTTL — interceptor

Oddiyroq usul — controller metodiga dekorator qo'shiladi:

// src/products/products.controller.ts
import { Controller, Get, Param, UseInterceptors } from '@nestjs/common';
import { CacheInterceptor, CacheKey, CacheTTL } from '@nestjs/cache-manager';

@Controller('products')
@UseInterceptors(CacheInterceptor) // ← barcha GET metodlarga
export class ProductsController {
  constructor(private productsService: ProductsService) {}

  @Get()
  @CacheKey('all-products')   // ← cache kaliti
  @CacheTTL(300_000)          // ← 5 daqiqa
  findAll() {
    return this.productsService.findAll({});
  }

  @Get('featured')
  @CacheKey('featured-products')
  @CacheTTL(600_000)          // ← 10 daqiqa
  findFeatured() {
    return this.productsService.findFeatured();
  }

  // Cache ishlatilmaydi (POST, PUT, DELETE — avtomatik)
  @Post()
  create(@Body() dto: CreateProductDto) {
    return this.productsService.create(dto);
  }
}

CacheInterceptor faqat GET so'rovlarni keshleydi. POST, PUT, DELETE — avtomatik o'tkazib yuboriladi.


Cache strategiyalari

Cache-Aside (yuqoridagi misollar):
  1. Cacheda qidirish
  2. Yo'q → bazadan olish
  3. Cachega yozish
  ✓ Eng ko'p ishlatiladi

Write-Through:
  Ma'lumot yozilganda ham cache ham baza yangilanadi
  ✓ Cache har doim yangi
  - Yozish sekinlashadi

Cache Invalidation:
  Ma'lumot o'zgarganda — tegishli cache lar o'chiriladi
  ✓ findAll() dan keyin create() bo'lsa — cache tozalanadi

Cache kalitlari strategiyasi

// Yaxshi kalit nomlash — "resurs:identifikator:variant"
`products`                          // barcha mahsulotlar
`products:category:electronics`     // elektronika kategoriya
`product:64a...`                    // bitta mahsulot
`user:64b...:profile`               // foydalanuvchi profili
`search:telefon:page:1:limit:10`    // qidiruv natijalari

// ❌ Yomon — noaniq, to'qnashishi mumkin
`data`
`list`
`result`

Tez-tez uchraydigan xatolar

Cache invalidation yo'q — eski ma'lumot ko'rsatiladi

// ❌ Ma'lumot yangilandi, lekin cache eski — 5 daqiqa eski mahsulot ko'rinadi
async update(id, dto) {
  return this.productModel.findByIdAndUpdate(id, dto, { new: true }).exec();
  // cache tozalanmadi!
}

// ✅ Yangilananda cache o'chiriladi
async update(id, dto) {
  const product = await this.productModel.findByIdAndUpdate(id, dto, { new: true }).exec();
  await this.cache.del(`product:${id}`);
  return product;
}

Katta ob'ektlarni keshlash

// ❌ 10MB json — Redis da joy egallaydi, serializatsiya sekin
await this.cache.set('all-users', hugeData, 300_000);

// ✅ Faqat kerakli ma'lumotni keshlash, yoki pagination bilan
await this.cache.set('users:page:1', pageData, 300_000);

TTL ni ms yoki soniyada adashish

// @nestjs/cache-manager v2+ → millisekunda
await this.cache.set(key, data, 300_000); // 5 daqiqa (ms)

// Eski versiya → soniya
await this.cache.set(key, data, 300); // 5 daqiqa (s)
// ← versiyaga qarab farq qiladi, docs ni tekshiring

Xulosa

Caching — bazaga keraksiz so'rovlarni kamaytirish. Ko'p o'qiladigan, kam o'zgaradigan ma'lumotlar uchun.

Eslab qoling:

  • CACHE_MANAGER inject — qo'lda boshqarish (moslashuvchan)
  • CacheInterceptor — dekorator bilan (oddiy)
  • cache.get(key) → cache.set(key, val, ttl) → cache.del(key)
  • Ma'lumot o'zgarganda — tegishli cache del qilinadi
  • Kalit nomi: resurs:id:variant formatida
  • Dev — xotira cache, Prod — Redis

Keyingi dars

34-dars: Helmet va CORS sozlash