Caching trong NestJS: Tối ưu hóa hiệu suất với Redis

By Thái Nguyễn
Picture of the author
Published on
nestjs and redis

Caching là một trong những kỹ thuật quan trọng nhất trong việc tối ưu hóa hiệu suất ứng dụng. Trong bài viết này, chúng ta sẽ tìm hiểu chi tiết cách tích hợp caching trong NestJS với Redis để tối ưu hóa tốc độ xử lý và nâng cao trải nghiệm người dùng.

Nội dung


1. Tại sao cần caching?

  • Tăng hiệu suất: Giảm thời gian phản hồi khi truy xuất dữ liệu.
  • Giảm tải cơ sở dữ liệu: Hạn chế số lượng truy vấn database.
  • Tăng trải nghiệm người dùng: Giúp tối ưu hóa tốc độ tải trang.

2. Redis là gì và tại sao chọn Redis?

Redis là một cơ sở dữ liệu NoSQL dưới dạng key-value, hoạt động trên bộ nhớ RAM, giúp truy xuất nhanh chóng.

Điểm nổi bật của Redis:

  • Hiệu suất cao: Hỗ trợ hàng triệu yêu cầu mỗi giây.
  • Hỗ trợ đa dạng: Lưu trữ nhiều dạng dữ liệu như chuỗi, danh sách, hash, set.
  • Phân tán: Hỗ trợ clustering, tối ưu cho hệ thống lớn.

3. Tích hợp caching trong NestJS với Redis

Bước 1: Cài đặt các thư viện

Đầu tiên, cài đặt các thư viện cần thiết:

npm install cache-manager cache-manager-redis-store ioredis

Bước 2: Cấu hình CacheModule

NestJS cung cấp CacheModule để làm việc với caching. Cách tích hợp Redis như sau:

import { Module, CacheModule } from '@nestjs/common';
import * as redisStore from 'cache-manager-redis-store';

@Module({
  imports: [
    CacheModule.register({
      store: redisStore,
      host: 'localhost', // Địa chỉ Redis server
      port: 6379,        // Cổng mặc định của Redis
      ttl: 60,           // Thời gian tồn tại cache (giây)
    }),
  ],
})
export class AppModule {}

Bước 3: Sử dụng CacheService

Dưới đây là cách lưu và lấy dữ liệu từ cache:

import { Injectable, CacheService } from '@nestjs/common';

@Injectable()
export class MyService {
  constructor(private readonly cacheService: CacheService) {}

  async getData(key: string): Promise<any> {
    // Kiểm tra dữ liệu trong cache
    const cachedData = await this.cacheService.get(key);
    if (cachedData) {
      return cachedData;
    }

    // Lấy dữ liệu từ database
    const data = await this.fetchFromDatabase(key);

    // Lưu vào cache
    await this.cacheService.set(key, data, { ttl: 120 }); // Cache tồn tại trong 120 giây

    return data;
  }

  private async fetchFromDatabase(key: string): Promise<any> {
    // Giả lập truy vấn database
    return { key, value: 'Dữ liệu từ database' };
  }
}

Bước 4: Sử dụng Decorator @Cacheable()

NestJS hỗ trợ decorator @Cacheable() để tự động caching.

import { Injectable } from '@nestjs/common';
import { Cacheable } from '@nestjs/cache-manager';

@Injectable()
export class MyService {
  @Cacheable({ ttl: 300 }) // Cache trong 300 giây
  async fetchData(id: number): Promise<any> {
    console.log('Fetching data from database...');
    return { id, name: 'Example' };
  }
}

4. Chiến lược caching nâng cao

4.1 Cache Invalidation

Cache invalidation là quá trình xóa cache khi dữ liệu gốc thay đổi.

  • TTL (Time-To-Live): Quy định thời gian tồn tại của cache, giúp tự động xóa cache khi hết hạn.
  • Manual Invalidation: Xóa thủ công khi dữ liệu được cập nhật hoặc xóa. Ví dụ:
await this.cacheService.del('key-to-remove');

4.2 Cache Hierarchy

Sử dụng nhiều lớp cache:

  • In-Memory Cache: Lưu dữ liệu thường xuyên được truy cập.
  • Distributed Cache (Redis): Lưu dữ liệu cho các máy chủ trong hệ thống phân tán.

4.3 Cache Stampede

Giảm thiểu tình trạng nhiều yêu cầu đồng thời làm quá tải hệ thống cache. Cách tiếp cận:

  • Locking: Sử dụng Redis lock để đảm bảo chỉ một yêu cầu được thực thi tại một thời điểm.
  • Randomized TTL: Thêm một khoảng thời gian ngẫu nhiên vào TTL để tránh việc tất cả cache hết hạn cùng lúc.

5. Ứng dụng thực tế: Hệ thống ngân hàng

Trong hệ thống ngân hàng, caching đóng vai trò quan trọng trong việc tối ưu hóa hiệu suất và giảm tải cho cơ sở dữ liệu.

Ví dụ: Lấy danh sách chi nhánh ngân hàng

Danh sách chi nhánh ngân hàng thường ít thay đổi và được sử dụng trong nhiều yêu cầu khác nhau, như khi người dùng tìm kiếm chi nhánh gần nhất. Dữ liệu này rất phù hợp để lưu vào Redis.

Cách triển khai:

import { Injectable, CacheService } from '@nestjs/common';

@Injectable()
export class BranchService {
  constructor(private readonly cacheService: CacheService) {}

  async getBranchList(): Promise<any[]> {
    const cacheKey = 'branch-list';

    // Kiểm tra cache
    const cachedBranches = await this.cacheService.get<any[]>(cacheKey);
    if (cachedBranches) {
      return cachedBranches;
    }

    // Truy vấn cơ sở dữ liệu (giả lập)
    const branches = await this.queryDatabaseForBranches();

    // Lưu vào cache
    await this.cacheService.set(cacheKey, branches, { ttl: 3600 }); // Cache 1 giờ

    return branches;
  }

  private async queryDatabaseForBranches(): Promise<any[]> {
    // Giả lập truy vấn database
    console.log('Querying database for branch list...');
    return [
      { id: 1, name: 'Chi nhánh Hà Nội', address: 'Số 1, Đường ABC, Hà Nội' },
      { id: 2, name: 'Chi nhánh TP HCM', address: 'Số 2, Đường XYZ, TP HCM' },
    ];
  }
}

Kết quả:

  • Lần gọi đầu tiên: Truy vấn cơ sở dữ liệu và lưu vào cache.
  • Các lần sau: Lấy dữ liệu từ Redis, giảm tải cho hệ thống database.

6. Theo dõi và quản lý dữ liệu trong Redis

6.1 Kiểm tra dữ liệu trong Redis bằng Redis CLI

Redis cung cấp một công cụ CLI để kiểm tra dữ liệu:

  1. Kết nối đến Redis server:

    redis-cli
    
  2. Liệt kê các key trong Redis:

    keys *
    
  3. Xem giá trị của một key cụ thể:

    get <key>
    
  4. Xóa một key cụ thể:

    del <key>
    

6.2 Sử dụng RedisInsight để quản lý dữ liệu

RedisInsight là một công cụ GUI cho phép bạn:

  • Xem danh sách key: Dễ dàng duyệt qua các key đang lưu trong Redis.
  • Kiểm tra giá trị: Click vào một key để xem giá trị chi tiết.
  • Xóa hoặc cập nhật dữ liệu: Hỗ trợ thao tác xóa hoặc chỉnh sửa giá trị của key.

6.3 Theo dõi dữ liệu động với lệnh monitor

Redis hỗ trợ lệnh monitor để xem các thao tác đang diễn ra trên server:

redis-cli monitor

Lệnh này sẽ hiển thị mọi thao tác đọc, ghi, hoặc xóa trên Redis server, rất hữu ích để debug.


7. Kết luận

Caching là một kỹ thuật mạnh mẽ để tối ưu hóa hiệu suất ứng dụng. Khi tích hợp Redis với NestJS, bạn có thể tận dụng khả năng lưu trữ nhanh chóng và linh hoạt của Redis để cải thiện trải nghiệm người dùng.