NestJS và Redis năm 2026: Caching, Sessions và Câu hỏi Phỏng vấn

Hướng dẫn toàn diện về tích hợp NestJS với Redis cho caching và quản lý session, kèm theo các câu hỏi phỏng vấn để chuẩn bị cho buổi phỏng vấn Node.js.

Tích hợp NestJS với Redis cho caching và quản lý session

Tích hợp NestJS với Redis cải thiện đáng kể hiệu năng ứng dụng bằng cách thay thế các truy vấn database bằng việc tìm kiếm trong bộ nhớ. Sự kết hợp giữa @nestjs/cache-manager cho caching và express-session với connect-redis cho việc lưu trữ session bao quát hai trường hợp sử dụng Redis phổ biến nhất trong các ứng dụng backend.

Điểm Quan Trọng

Caching Redis trong NestJS giảm tải database bằng cách lưu trữ dữ liệu thường xuyên truy cập trong bộ nhớ. Quản lý session với Redis cho phép horizontal scaling bằng cách chia sẻ trạng thái session giữa nhiều instance.

Thiết lập @nestjs/cache-manager với Redis

Module cache-manager được chuyển sang package riêng từ NestJS 10. Việc cài đặt yêu cầu cả wrapper NestJS và cache store nền tảng. Adapter @keyv/redis là phương pháp được khuyến nghị cho việc tích hợp Redis với cache-manager v5.

bash
# Cài đặt các package cần thiết
npm install @nestjs/cache-manager cache-manager @keyv/redis

Việc đăng ký module cấu hình kết nối Redis và TTL mặc định. Tùy chọn isGlobal làm cho cache có sẵn trên tất cả các module mà không cần import lại.

app.module.tstypescript
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import KeyvRedis from '@keyv/redis';

@Module({
  imports: [
    CacheModule.registerAsync({
      isGlobal: true,
      useFactory: () => ({
        stores: [
          new KeyvRedis(process.env.REDIS_URL || 'redis://localhost:6379'),
        ],
        ttl: 60000, // TTL mặc định tính bằng mili giây
      }),
    }),
  ],
})
export class AppModule {}

Mảng stores chấp nhận nhiều cache store cho multi-tier caching. Môi trường production thường sử dụng Redis làm store chính, với fallback in-memory tùy chọn cho development.

Cache Injection và Service-Level Caching

Token CACHE_MANAGER cung cấp quyền truy cập trực tiếp vào cache cho các hoạt động ở cấp service. Phương pháp này cung cấp nhiều kiểm soát hơn so với HTTP caching tự động khi business logic quyết định thời điểm invalidate cache.

users.service.tstypescript
import { Injectable, Inject } from '@nestjs/common';
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
import { UsersRepository } from './users.repository';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  constructor(
    @Inject(CACHE_MANAGER) private cache: Cache,
    private usersRepository: UsersRepository,
  ) {}

  async findById(id: string): Promise<User | null> {
    // Kiểm tra cache trước
    const cacheKey = `user:${id}`;
    const cached = await this.cache.get<User>(cacheKey);
    
    if (cached) {
      return cached;
    }

    // Cache miss: lấy từ database
    const user = await this.usersRepository.findById(id);
    
    if (user) {
      await this.cache.set(cacheKey, user, 300000); // TTL 5 phút
    }

    return user;
  }

  async update(id: string, data: Partial<User>): Promise<User> {
    const user = await this.usersRepository.update(id, data);
    
    // Invalidate cache sau khi update
    await this.cache.del(`user:${id}`);
    
    return user;
  }

  async delete(id: string): Promise<void> {
    await this.usersRepository.delete(id);
    await this.cache.del(`user:${id}`);
  }
}

Mẫu cache-aside được triển khai ở trên kiểm tra cache trước khi truy vấn database. Việc invalidation xảy ra trong các hoạt động write để duy trì tính nhất quán dữ liệu giữa cache và nguồn dữ liệu gốc.

HTTP Response Caching với Interceptor

NestJS cung cấp CacheInterceptor để tự động cache các phản hồi HTTP. Các decorator @CacheTTL()@CacheKey() tùy chỉnh hành vi cho từng endpoint.

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

@Controller('products')
@UseInterceptors(CacheInterceptor)
export class ProductsController {
  constructor(private productsService: ProductsService) {}

  @Get()
  @CacheTTL(120000) // 2 phút cho danh sách sản phẩm
  async findAll() {
    return this.productsService.findAll();
  }

  @Get(':id')
  @CacheTTL(300000) // 5 phút cho sản phẩm riêng lẻ
  async findOne(@Param('id') id: string) {
    return this.productsService.findById(id);
  }

  @Get('category/:category')
  @CacheKey('products-by-category')
  @CacheTTL(180000)
  async findByCategory(@Param('category') category: string) {
    return this.productsService.findByCategory(category);
  }
}

Interceptor tự động cache các phản hồi GET sử dụng URL request làm key mặc định. Custom cache key hữu ích khi nhiều route cần chia sẻ hoặc có các entry cache khác nhau.

Quản lý Session với Redis Store

Session HTTP được hỗ trợ bởi Redis cho phép dữ liệu session tồn tại qua các lần khởi động lại server và horizontal scaling. Package express-session tích hợp với NestJS thông qua middleware.

bash
npm install express-session connect-redis ioredis
npm install -D @types/express-session

Cấu hình session store yêu cầu một instance Redis client. Package ioredis cung cấp hỗ trợ Redis tốt hơn với clustering và sentinel.

main.tstypescript
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as session from 'express-session';
import RedisStore from 'connect-redis';
import { Redis } from 'ioredis';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const redisClient = new Redis(
    process.env.REDIS_URL || 'redis://localhost:6379'
  );

  app.use(
    session({
      store: new RedisStore({ client: redisClient }),
      secret: process.env.SESSION_SECRET || 'your-secret-key',
      resave: false,
      saveUninitialized: false,
      cookie: {
        secure: process.env.NODE_ENV === 'production',
        httpOnly: true,
        maxAge: 24 * 60 * 60 * 1000, // 24 giờ
        sameSite: 'lax',
      },
    }),
  );

  await app.listen(3000);
}
bootstrap();

Tùy chọn resave: false ngăn session được lưu lại nếu không bị thay đổi. Cài đặt saveUninitialized: false đảm bảo các session trống không làm đầy bộ nhớ Redis.

Truy cập Session Data trong Controller

Dữ liệu session có sẵn thông qua đối tượng request. Custom decorator đơn giản hóa việc truy cập session trên các handler.

session.decorator.tstypescript
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const GetSession = createParamDecorator(
  (data: string, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return data ? request.session?.[data] : request.session;
  },
);

// auth.controller.ts
import { Controller, Post, Body, Get } from '@nestjs/common';
import { GetSession } from './session.decorator';
import { Session } from 'express-session';

interface UserSession extends Session {
  userId?: string;
  role?: string;
  loginAt?: Date;
}

@Controller('auth')
export class AuthController {
  @Post('login')
  async login(
    @Body() credentials: LoginDto,
    @GetSession() session: UserSession,
  ) {
    // Xác thực thông tin đăng nhập
    const user = await this.authService.validate(credentials);
    
    // Lưu dữ liệu user vào session
    session.userId = user.id;
    session.role = user.role;
    session.loginAt = new Date();
    
    return { message: 'Đăng nhập thành công' };
  }

  @Post('logout')
  async logout(@GetSession() session: UserSession) {
    return new Promise((resolve, reject) => {
      session.destroy((err) => {
        if (err) reject(err);
        resolve({ message: 'Đăng xuất thành công' });
      });
    });
  }

  @Get('profile')
  async getProfile(@GetSession('userId') userId: string) {
    if (!userId) {
      throw new UnauthorizedException('Session không hợp lệ');
    }
    return this.usersService.findById(userId);
  }
}

Chiến lược Invalidate Cache

Chiến lược invalidate cache phù hợp duy trì tính nhất quán dữ liệu. Các mẫu phổ biến bao gồm time-based expiration, event-driven invalidation và manual purging.

cache-invalidation.service.tstypescript
import { Injectable, Inject } from '@nestjs/common';
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
import { OnEvent } from '@nestjs/event-emitter';

@Injectable()
export class CacheInvalidationService {
  constructor(@Inject(CACHE_MANAGER) private cache: Cache) {}

  // Event-driven invalidation
  @OnEvent('user.updated')
  async handleUserUpdate(payload: { userId: string }) {
    await this.cache.del(`user:${payload.userId}`);
    await this.cache.del('users:list');
  }

  @OnEvent('product.created')
  async handleProductCreate() {
    // Invalidate tất cả cache sản phẩm khi sản phẩm mới được tạo
    await this.invalidatePattern('products:*');
  }

  // Pattern-based invalidation yêu cầu truy cập Redis trực tiếp
  async invalidatePattern(pattern: string): Promise<void> {
    const store = this.cache.stores[0] as any;
    const redis = store?.opts?.redis || store?.redis;
    
    if (redis) {
      const keys = await redis.keys(pattern);
      if (keys.length > 0) {
        await redis.del(...keys);
      }
    }
  }

  // Manual cache warming
  async warmCache(): Promise<void> {
    const popularProducts = await this.productsService.findPopular();
    
    for (const product of popularProducts) {
      await this.cache.set(`product:${product.id}`, product, 600000);
    }
  }
}

Rate Limiting với Redis

Redis cung cấp bộ nhớ phân tán cho các rate limit counter. Package @nestjs/throttler hỗ trợ Redis làm backend lưu trữ.

bash
npm install @nestjs/throttler @nestjs/throttler-storage-redis ioredis
app.module.tstypescript
import { Module } from '@nestjs/common';
import { ThrottlerModule } from '@nestjs/throttler';
import { ThrottlerStorageRedisService } from '@nestjs/throttler-storage-redis';
import { Redis } from 'ioredis';

@Module({
  imports: [
    ThrottlerModule.forRootAsync({
      useFactory: () => ({
        throttlers: [
          {
            name: 'short',
            ttl: 1000,
            limit: 3,
          },
          {
            name: 'medium',
            ttl: 10000,
            limit: 20,
          },
          {
            name: 'long',
            ttl: 60000,
            limit: 100,
          },
        ],
        storage: new ThrottlerStorageRedisService(
          new Redis(process.env.REDIS_URL || 'redis://localhost:6379')
        ),
      }),
    }),
  ],
})
export class AppModule {}

Sẵn sàng chinh phục phỏng vấn Node.js / NestJS?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Câu hỏi Phỏng vấn: Caching và Redis trong NestJS

Dưới đây là các câu hỏi phỏng vấn phổ biến để đánh giá kiến thức về tích hợp Redis trong ứng dụng NestJS.

Câu hỏi 1: Sự khác biệt giữa cache-aside và read-through caching là gì?

Cache-aside yêu cầu code ứng dụng kiểm tra cache một cách rõ ràng và điền dữ liệu khi cache miss. Read-through caching ủy quyền quá trình này cho cache layer. NestJS cache-manager sử dụng mẫu cache-aside trong đó developer chịu trách nhiệm về logic get/set.

Câu hỏi 2: Làm thế nào để xử lý cache stampede?

Cache stampede xảy ra khi nhiều request đồng thời gặp cache miss và tất cả đều truy vấn database. Các giải pháp bao gồm request coalescing (xếp hàng các request trùng lặp), probabilistic early expiration, hoặc lock-based population trong đó chỉ một request điền cache trong khi các request khác chờ đợi.

Câu hỏi 3: Tại sao sử dụng Redis cho session thay vì session dựa trên bộ nhớ?

Session dựa trên bộ nhớ không tồn tại qua các lần khởi động lại server và không thể chia sẻ giữa nhiều instance. Redis cung cấp tính bền vững, horizontal scaling và các tính năng expiration tích hợp cho quản lý session.

Câu hỏi 4: Giải thích sự khác biệt giữa cache TTL và sliding expiration?

TTL cố định hết hạn entry tại khoảng thời gian đã đặt kể từ khi tạo. Sliding expiration đặt lại bộ đếm thời gian mỗi khi dữ liệu được truy cập. Redis gốc chỉ hỗ trợ TTL cố định; sliding expiration yêu cầu logic ứng dụng để cập nhật thời gian expiration khi đọc.

Câu hỏi 5: Làm thế nào để cache dữ liệu khác nhau cho mỗi user?

Sử dụng cache key bao gồm user identifier: user:${userId}:preferences. Điều này đảm bảo sự cách ly giữa dữ liệu của các user khác nhau trong khi vẫn được hưởng lợi từ caching.

Câu hỏi 6: Sự đánh đổi giữa cache hit rate và memory usage là gì?

TTL cao hơn tăng hit rate nhưng tiêu thụ nhiều bộ nhớ hơn và có nguy cơ dữ liệu trở nên cũ. TTL ngắn hơn giữ dữ liệu mới nhưng tăng tải database. Giải pháp tối ưu cân bằng cả hai yếu tố dựa trên mẫu truy cập và độ nhạy cảm của dữ liệu.

Kết luận

Tích hợp Redis với NestJS yêu cầu hiểu biết về các mẫu caching, chiến lược quản lý session và các cân nhắc cấu hình. Module @nestjs/cache-manager cung cấp một abstraction rõ ràng cho các hoạt động caching, trong khi express-session với connect-redis xử lý việc lưu trữ session. Sự kết hợp của cả hai cho phép ứng dụng NestJS scale theo chiều ngang trong khi duy trì hiệu năng và quản lý trạng thái nhất quán.

Thử thách hôm nay

Bạn có tìm ra lỗi trong Node.js / NestJS không?

Một đoạn mã thật, một lỗi ẩn, mỗi ngày một lượt. Không cần tài khoản để thử.

Anthony Fillion-Maillet

Viết bởi

Anthony Fillion-Maillet

Người sáng lập SharpSkill

Lập trình viên fullstack hơn 10 năm. Anh điều hành SharpSkill và chịu trách nhiệm về mọi nội dung đăng tại đây.

Cập nhật ngày 10 tháng 9, 2026

Thẻ

#nestjs
#redis
#caching
#nodejs
#interview

Chia sẻ

Bài viết liên quan