title: NestJS缓存机制
description: 深入学习NestJS中的缓存机制实现,包括缓存模块、缓存策略、缓存失效和Redis集成
keywords: NestJS, 缓存机制, 缓存模块, 缓存策略, 缓存失效, Redis集成, 性能优化

NestJS缓存机制

学习目标

通过本章节的学习,你将能够:

  • 理解缓存的基本概念和工作原理
  • 掌握NestJS中缓存模块的使用方法
  • 实现不同的缓存策略
  • 理解缓存失效的处理方法
  • 集成Redis作为缓存存储
  • 构建高性能的缓存系统
  • 理解缓存的最佳实践和常见陷阱

核心知识点

缓存基础

缓存是一种存储临时数据的机制,用于提高应用性能和减少对底层资源的访问。缓存的主要优势包括:

  • 提高响应速度:从缓存中读取数据比从数据库或其他存储中读取更快
  • 减少资源消耗:减少对数据库、API等的请求次数
  • 提高可扩展性:减轻系统负载,支持更多并发用户

NestJS缓存模块

NestJS提供了一个内置的缓存模块 @nestjs/cache-manager,它基于 cache-manager 库,支持多种缓存存储后端。缓存模块的主要特性包括:

  • 支持内存缓存(默认)
  • 支持Redis等外部缓存存储
  • 提供声明式缓存装饰器
  • 支持缓存失效和TTL(生存时间)
  • 支持缓存键自定义

缓存策略

常见的缓存策略包括:

  • 缓存优先:首先检查缓存,如果存在则返回,否则从数据源获取并更新缓存
  • 数据源优先:始终从数据源获取,然后更新缓存
  • 写入穿透:写入时同时更新缓存和数据源
  • 写入回写:写入时只更新缓存,定期将缓存数据同步到数据源
  • 写入绕过:写入时只更新数据源,不更新缓存

缓存失效

缓存失效是指当数据源发生变化时,确保缓存中的数据也相应更新的过程。常见的缓存失效策略包括:

  • 时间失效:为缓存设置TTL,过期后自动失效
  • 主动失效:当数据源更新时,主动删除或更新对应的缓存
  • 版本控制:使用版本号或时间戳作为缓存键的一部分
  • 缓存穿透保护:处理不存在的数据请求,避免缓存被绕过

Redis集成

Redis是一种高性能的内存数据存储,常被用作缓存后端。Redis的主要优势包括:

  • 高性能:基于内存操作,速度极快
  • 持久化:支持数据持久化到磁盘
  • 丰富的数据结构:支持字符串、哈希、列表、集合等
  • 分布式:支持集群部署,提高可用性和容量
  • 发布/订阅:支持消息通知机制

实用案例分析

案例:高性能API缓存系统

我们将构建一个高性能的API缓存系统,使用Redis作为缓存存储,实现数据缓存和缓存失效处理。

1. 安装依赖

首先,我们需要安装必要的依赖:

npm install @nestjs/cache-manager cache-manager
npm install cache-manager-redis-store redis

2. 配置缓存模块

AppModule中配置缓存模块:

// src/app.module.ts
import { Module, CacheModule } from '@nestjs/common';
import * as redisStore from 'cache-manager-redis-store';
import { UserModule } from './user/user.module';
import { ArticleModule } from './article/article.module';

@Module({
  imports: [
    CacheModule.register({
      store: redisStore,
      host: 'localhost',
      port: 6379,
      ttl: 60, // 缓存过期时间(秒)
    }),
    UserModule,
    ArticleModule,
  ],
})
export class AppModule {} 

3. 使用缓存装饰器

在控制器中使用缓存装饰器:

// src/article/article.controller.ts
import { Controller, Get, Post, Put, Delete, Param, UseInterceptors, CacheInterceptor, CacheKey, CacheTTL } from '@nestjs/common';
import { ArticleService } from './article.service';
import { Article } from './entities/article.entity';

@Controller('article')
export class ArticleController {
  constructor(private articleService: ArticleService) {}

  // 使用默认缓存配置
  @UseInterceptors(CacheInterceptor)
  @Get()
  async getAllArticles() {
    return this.articleService.findAll();
  }

  // 自定义缓存键和TTL
  @CacheKey('article-detail')
  @CacheTTL(300) // 5分钟
  @UseInterceptors(CacheInterceptor)
  @Get(':id')
  async getArticleById(@Param('id') id: number) {
    return this.articleService.findOne(id);
  }

  // 不使用缓存
  @Get('no-cache')
  async getArticlesNoCache() {
    return this.articleService.findAll();
  }

  // 创建文章(会自动使相关缓存失效)
  @Post()
  async createArticle(@Body() articleData: Partial<Article>) {
    return this.articleService.create(articleData);
  }

  // 更新文章(会自动使相关缓存失效)
  @Put(':id')
  async updateArticle(@Param('id') id: number, @Body() updates: Partial<Article>) {
    return this.articleService.update(id, updates);
  }

  // 删除文章(会自动使相关缓存失效)
  @Delete(':id')
  async deleteArticle(@Param('id') id: number) {
    return this.articleService.delete(id);
  }
}

4. 自定义缓存键生成器

创建一个自定义的缓存键生成器,以支持更复杂的缓存键策略:

// src/cache/custom-cache-key-generator.ts
import { CacheKeyGenerator } from '@nestjs/cache-manager';
import { ExecutionContext } from '@nestjs/common';

export class CustomCacheKeyGenerator implements CacheKeyGenerator {
  generate(context: ExecutionContext): string {
    const request = context.switchToHttp().getRequest();
    const { httpAdapter } = this.httpAdapterHost;
    const requestUrl = httpAdapter.getRequestUrl(request);
    const requestMethod = request.method;
    
    // 包含请求方法和URL作为缓存键的一部分
    return `${requestMethod}:${requestUrl}`;
  }

  constructor(private httpAdapterHost: any) {}
}

5. 配置自定义缓存键生成器

在模块中配置自定义缓存键生成器:

// src/article/article.module.ts
import { Module, HttpAdapterHost } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { ArticleService } from './article.service';
import { ArticleController } from './article.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Article } from './entities/article.entity';
import { CustomCacheKeyGenerator } from '../cache/custom-cache-key-generator';

@Module({
  imports: [
    TypeOrmModule.forFeature([Article]),
    CacheModule.register(),
  ],
  providers: [
    ArticleService,
    {
      provide: 'CACHE_KEY_GENERATOR',
      useClass: CustomCacheKeyGenerator,
      inject: [HttpAdapterHost],
    },
  ],
  controllers: [ArticleController],
})
export class ArticleModule {} 

6. 使用缓存管理器

在服务中直接使用缓存管理器,实现更精细的缓存控制:

// src/article/article.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Cache } from 'cache-manager';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Article } from './entities/article.entity';

@Injectable()
export class ArticleService {
  constructor(
    @InjectRepository(Article) 
    private articleRepository: Repository<Article>,
    @Inject(CACHE_MANAGER) 
    private cacheManager: Cache,
  ) {}

  // 查找所有文章,使用缓存
  async findAll() {
    const cacheKey = 'articles:all';
    
    // 尝试从缓存获取
    const cachedArticles = await this.cacheManager.get(cacheKey);
    if (cachedArticles) {
      return cachedArticles;
    }
    
    // 从数据库获取
    const articles = await this.articleRepository.find();
    
    // 更新缓存,设置10分钟过期
    await this.cacheManager.set(cacheKey, articles, { ttl: 600 });
    
    return articles;
  }

  // 查找单个文章,使用缓存
  async findOne(id: number) {
    const cacheKey = `article:${id}`;
    
    // 尝试从缓存获取
    const cachedArticle = await this.cacheManager.get(cacheKey);
    if (cachedArticle) {
      return cachedArticle;
    }
    
    // 从数据库获取
    const article = await this.articleRepository.findOne({ where: { id } });
    
    // 更新缓存,设置5分钟过期
    if (article) {
      await this.cacheManager.set(cacheKey, article, { ttl: 300 });
    }
    
    return article;
  }

  // 创建文章,更新缓存
  async create(articleData: Partial<Article>) {
    const article = this.articleRepository.create(articleData);
    const savedArticle = await this.articleRepository.save(article);
    
    // 使相关缓存失效
    await this.cacheManager.del('articles:all');
    
    return savedArticle;
  }

  // 更新文章,更新缓存
  async update(id: number, updates: Partial<Article>) {
    await this.articleRepository.update(id, updates);
    const updatedArticle = await this.articleRepository.findOne({ where: { id } });
    
    // 使相关缓存失效
    await this.cacheManager.del('articles:all');
    await this.cacheManager.del(`article:${id}`);
    
    return updatedArticle;
  }

  // 删除文章,更新缓存
  async delete(id: number) {
    const result = await this.articleRepository.delete(id);
    
    // 使相关缓存失效
    await this.cacheManager.del('articles:all');
    await this.cacheManager.del(`article:${id}`);
    
    return result;
  }
}

7. 实现缓存拦截器

创建一个自定义的缓存拦截器,实现更复杂的缓存逻辑:

// src/cache/custom-cache.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Cache } from 'cache-manager';
import { CACHE_MANAGER } from '@nestjs/cache-manager';

@Injectable()
export class CustomCacheInterceptor implements NestInterceptor {
  constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}

  async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
    const request = context.switchToHttp().getRequest();
    const cacheKey = `custom:${request.method}:${request.url}`;

    // 尝试从缓存获取
    const cachedData = await this.cacheManager.get(cacheKey);
    if (cachedData) {
      return new Observable(observer => {
        observer.next(cachedData);
        observer.complete();
      });
    }

    // 执行请求
    return next.handle().pipe(
      tap(data => {
        // 更新缓存,设置3分钟过期
        this.cacheManager.set(cacheKey, data, { ttl: 180 });
      }),
    );
  }
}

8. 使用自定义缓存拦截器

在控制器中使用自定义缓存拦截器:

// src/user/user.controller.ts
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { CustomCacheInterceptor } from '../cache/custom-cache.interceptor';
import { UserService } from './user.service';

@Controller('user')
export class UserController {
  constructor(private userService: UserService) {}

  // 使用自定义缓存拦截器
  @UseInterceptors(CustomCacheInterceptor)
  @Get()
  async getAllUsers() {
    return this.userService.findAll();
  }
}

代码优化建议

  1. 缓存键设计

    • 使用有意义的缓存键命名规范
    • 包含请求参数和上下文信息
    • 避免使用过长的缓存键
    • 考虑使用缓存键前缀区分不同类型的缓存
  2. 缓存策略优化

    • 根据数据访问模式选择合适的缓存策略
    • 为不同类型的数据设置不同的TTL
    • 考虑使用缓存预热,提前加载热点数据
    • 实现缓存监控,了解缓存命中率
  3. 性能优化

    • 使用Redis等外部缓存存储,避免内存溢出
    • 实现缓存分片,分散缓存压力
    • 考虑使用批量操作减少缓存访问次数
    • 实现缓存穿透保护,避免缓存被绕过
  4. 可靠性优化

    • 实现缓存降级,当缓存不可用时回退到数据源
    • 监控缓存健康状态
    • 实现缓存备份和恢复机制
    • 考虑使用Redis集群提高可用性
  5. 安全性优化

    • 避免在缓存中存储敏感信息
    • 实现缓存键的访问控制
    • 考虑使用加密存储敏感数据
    • 定期清理过期缓存数据

常见问题与解决方案

1. 缓存一致性问题

问题:缓存数据与数据源不一致

解决方案

  • 实现主动缓存失效
  • 使用合适的缓存策略
  • 为缓存设置合理的TTL
  • 考虑使用版本控制或时间戳

2. 缓存穿透

问题:恶意请求不存在的数据,导致缓存被绕过,直接访问数据源

解决方案

  • 实现布隆过滤器
  • 缓存空结果
  • 实施请求速率限制
  • 验证请求参数

3. 缓存雪崩

问题:大量缓存同时过期,导致请求突然全部落在数据源上

解决方案

  • 为缓存设置随机TTL
  • 实现缓存预热
  • 使用分层缓存
  • 实施请求限流

4. 缓存击穿

问题:热点数据缓存过期,导致大量请求同时访问数据源

解决方案

  • 使用互斥锁
  • 实现后台更新
  • 为热点数据设置永不过期
  • 考虑使用双层缓存

5. 内存溢出

问题:缓存数据过多,导致内存溢出

解决方案

  • 使用Redis等外部缓存存储
  • 设置合理的缓存大小限制
  • 实现缓存淘汰策略
  • 定期清理过期缓存

小结

本章节我们学习了NestJS中的缓存机制实现,包括:

  • 缓存的基本概念和工作原理
  • NestJS缓存模块的使用方法
  • 不同的缓存策略和实现
  • 缓存失效的处理方法
  • Redis集成作为缓存存储
  • 自定义缓存拦截器和键生成器
  • 缓存的最佳实践和常见问题解决方案

通过这些知识,你可以构建高性能、可靠的缓存系统,提高应用的响应速度和可扩展性,同时避免缓存相关的常见陷阱。

互动问答

  1. 问题:缓存的主要作用是什么?
    答案:缓存的主要作用是提高应用性能和减少对底层资源的访问,通过存储临时数据来加速数据访问速度。

  2. 问题:NestJS中如何使用缓存?
    答案:可以通过以下方式使用缓存:1. 使用@UseInterceptors(CacheInterceptor)装饰器;2. 使用@CacheKey和@CacheTTL装饰器自定义缓存行为;3. 直接注入CacheManager使用编程式缓存。

  3. 问题:什么是缓存失效?如何处理缓存失效?
    答案:缓存失效是指当数据源发生变化时,确保缓存中的数据也相应更新的过程。处理缓存失效的方法包括:设置TTL、主动删除缓存、使用版本控制等。

  4. 问题:为什么要使用Redis作为缓存存储?
    答案:Redis作为缓存存储的优势包括:高性能、支持持久化、丰富的数据结构、分布式支持、发布/订阅机制等。

  5. 问题:常见的缓存问题有哪些?如何解决?
    答案:常见的缓存问题包括:缓存一致性、缓存穿透、缓存雪崩、缓存击穿、内存溢出等。解决方案包括:主动缓存失效、布隆过滤器、随机TTL、互斥锁、使用Redis等外部缓存存储等。

实践作业

  1. 作业1:实现一个基于Redis的缓存系统,支持用户会话存储

  2. 作业2:实现缓存预热功能,在应用启动时加载热点数据到缓存

  3. 作业3:实现缓存监控,记录缓存命中率和性能指标

  4. 作业4:实现缓存降级机制,当Redis不可用时自动切换到内存缓存

  5. 作业5:构建一个完整的电商API,使用缓存优化产品列表和详情页的性能

通过完成这些作业,你将能够更加深入地理解缓存机制的实现细节,为构建高性能的应用打下坚实的基础。

« 上一篇 16-authorization 下一篇 » 18-file-upload