NestJS — Node.js للمؤسسات

التخزين المؤقت مع Redis

16 دقيقة الدرس 40 من 48

التخزين المؤقت مع Redis

يُعدّ التخزين المؤقت (Caching) من أكثر تقنيات الأداء أثرًا في متناول مهندس الواجهة الخلفية. بدلًا من إعادة حساب البيانات أو جلبها في كل طلب، يحفظ التخزين المؤقت النتيجة ويعيدها فورًا في الطلبات اللاحقة. يُزوّد NestJS بوحدة CacheModule من الدرجة الأولى مبنيّة فوق cache-manager، وباستخدام Redis كمخزن خلفيّ تحصل على تخزين مؤقت موزَّع ودائم يظل حيًّا عبر إعادة التشغيل ويتوسّع عبر نُسَخ تطبيق متعدّدة.

تثبيت الاعتماديات

npm install @nestjs/cache-manager cache-manager npm install cache-manager-ioredis-yet ioredis npm install -D @types/cache-manager

cache-manager-ioredis-yet هو محوّل مخزن Redis الحديث لـ cache-manager الإصدار 5 وما فوق. يستخدم ioredis داخليًّا، وهو عميل Redis الموصى به لـ Node.js في بيئة الإنتاج.

تسجيل CacheModule مع Redis

استورد CacheModule في AppModule (أو أي وحدة ميزة). استخدم CacheModule.registerAsync() لسحب تفاصيل اتصال Redis من ConfigService:

import { Module } from '@nestjs/common'; import { CacheModule } from '@nestjs/cache-manager'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { redisStore } from 'cache-manager-ioredis-yet'; @Module({ imports: [ ConfigModule.forRoot(), CacheModule.registerAsync({ isGlobal: true, imports: [ConfigModule], inject: [ConfigService], useFactory: async (config: ConfigService) => ({ store: redisStore, host: config.get<string>('REDIS_HOST', 'localhost'), port: config.get<number>('REDIS_PORT', 6379), ttl: 60, // مدة الصلاحية الافتراضية بالثواني }), }), ], }) export class AppModule {}
isGlobal: true يجعل التخزين المؤقت متاحًا عبر كل وحدة دون الحاجة إلى إعادة استيراد CacheModule. في التطبيقات الكبيرة هذا ما تريده دائمًا تقريبًا.

CacheInterceptor — تخزين مسارات مؤقت دون كتابة نماذج زائدة

يوفّر NestJS الـ CacheInterceptor الذي يخزّن استجابة طريقة تحكّم كاملة تلقائيًّا. طبّقه على مستوى المتحكّم أو الطريقة عبر @UseInterceptors، أو اربطه عالميًّا عبر موفّر APP_INTERCEPTOR:

import { Controller, Get, UseInterceptors } from '@nestjs/common'; import { CacheInterceptor, CacheKey, CacheTTL } from '@nestjs/cache-manager'; @Controller('products') @UseInterceptors(CacheInterceptor) export class ProductsController { // يستخدم مدة الصلاحية الافتراضية (60 ث) ومفتاحًا مولَّدًا تلقائيًّا من URL المسار @Get() findAll() { return this.productsService.findAll(); } // تجاوز المفتاح ومدة الصلاحية لهذه النقطة النهائية تحديدًا @Get('featured') @CacheKey('products:featured') @CacheTTL(300) // 5 دقائق getFeatured() { return this.productsService.getFeatured(); } }

يُثبّت @CacheKey إدخال التخزين المؤقت على مفتاح نصيّ ثابت بصرف النظر عن معاملات URL. ويتجاوز @CacheTTL الإعداد الافتراضي على مستوى الوحدة لذلك المعالج فقط.

عمليات التخزين المؤقت اليدوية عبر CACHE_MANAGER

للتحكّم الدقيق — كتابة وقراءة وحذف مفاتيح فردية — حقن رمز مدير التخزين المؤقت مباشرةً:

import { Injectable, Inject } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; @Injectable() export class ProductsService { constructor(@Inject(CACHE_MANAGER) private cache: Cache) {} async findById(id: number) { const key = `product:${id}`; const cached = await this.cache.get<Product>(key); if (cached) return cached; const product = await this.repo.findOne({ where: { id } }); await this.cache.set(key, product, 120); // مدة الصلاحية 120 ث return product; } async update(id: number, dto: UpdateProductDto) { const product = await this.repo.save({ id, ...dto }); await this.cache.del(`product:${id}`); // إبطال عند الكتابة return product; } }

استراتيجيات إبطال التخزين المؤقت

  • انتهاء الصلاحية بالوقت (TTL) — أبسط استراتيجية؛ ينتهي صلاحية التخزين تلقائيًّا بعد N ثانية. مناسب عندما تكون البيانات القديمة قليلًا مقبولة (مثل قوائم المنتجات).
  • الإبطال المعتمد على الأحداث — استدعاء cache.del(key) صراحةً عند تغيّر البيانات الأساسية (تحديث/حذف). يضمن أن القراءة التالية تجلب بيانات جديدة فورًا.
  • الإبطال بالنطاق/العلامة — بادئة المفاتيح ذات الصلة (مثل products:*) والتكرار لحذف مجموعة كاملة. مفيد لإبطال عائلة موارد بأكملها دفعةً واحدة.
  • الكتابة المتزامنة (Write-through) — تحديث التخزين المؤقت وقاعدة البيانات في العملية ذاتها حتى لا يصبح التخزين المؤقت قديمًا؛ أكثر تعقيدًا قليلًا لكنه يضمن الاتساق.
لا تخزّن مؤقتًا أبدًا استجابات تحتوي على بيانات خاصة بمستخدم معيّن تحت مفتاح مشترك. إن شارك مستخدمان مفتاحًا واحدًا، قد يستقبل المستخدم أ بيانات المستخدم ب الخاصة. أدرج دائمًا معرّف المستخدم في المفتاح حين تكون الاستجابة مخصَّصة (مثل orders:${userId}).
استخدم بادئات مفاتيح Redis في الإنتاج. اضبط keyPrefix في إعداد المخزن (مثلًا 'myapp:') لتمييز مفاتيحك عن التطبيقات أو الخدمات الأخرى التي تشارك نفس نسخة Redis.

الخلاصة

تمنح CacheModule مع مخزن Redis تطبيقات NestJS تخزينًا مؤقتًا موزَّعًا ودائمًا. يتولّى CacheInterceptor التخزين المؤقت التلقائي على مستوى المسار؛ ويضبط @CacheKey و@CacheTTL النقاط النهائية الفردية. للعمليات اليدوية حقن CACHE_MANAGER واستدعاء get وset وdel. اختر دائمًا استراتيجية إبطال — TTL أو مُعتمِدة على أحداث أو كتابة متزامنة — تناسب متطلبات اتساق بياناتك، وأبقِ البيانات الخاصة بمستخدم خلف مفاتيح مرتبطة بذلك المستخدم.