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 redis2. 配置缓存模块
在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();
}
}代码优化建议
缓存键设计:
- 使用有意义的缓存键命名规范
- 包含请求参数和上下文信息
- 避免使用过长的缓存键
- 考虑使用缓存键前缀区分不同类型的缓存
缓存策略优化:
- 根据数据访问模式选择合适的缓存策略
- 为不同类型的数据设置不同的TTL
- 考虑使用缓存预热,提前加载热点数据
- 实现缓存监控,了解缓存命中率
性能优化:
- 使用Redis等外部缓存存储,避免内存溢出
- 实现缓存分片,分散缓存压力
- 考虑使用批量操作减少缓存访问次数
- 实现缓存穿透保护,避免缓存被绕过
可靠性优化:
- 实现缓存降级,当缓存不可用时回退到数据源
- 监控缓存健康状态
- 实现缓存备份和恢复机制
- 考虑使用Redis集群提高可用性
安全性优化:
- 避免在缓存中存储敏感信息
- 实现缓存键的访问控制
- 考虑使用加密存储敏感数据
- 定期清理过期缓存数据
常见问题与解决方案
1. 缓存一致性问题
问题:缓存数据与数据源不一致
解决方案:
- 实现主动缓存失效
- 使用合适的缓存策略
- 为缓存设置合理的TTL
- 考虑使用版本控制或时间戳
2. 缓存穿透
问题:恶意请求不存在的数据,导致缓存被绕过,直接访问数据源
解决方案:
- 实现布隆过滤器
- 缓存空结果
- 实施请求速率限制
- 验证请求参数
3. 缓存雪崩
问题:大量缓存同时过期,导致请求突然全部落在数据源上
解决方案:
- 为缓存设置随机TTL
- 实现缓存预热
- 使用分层缓存
- 实施请求限流
4. 缓存击穿
问题:热点数据缓存过期,导致大量请求同时访问数据源
解决方案:
- 使用互斥锁
- 实现后台更新
- 为热点数据设置永不过期
- 考虑使用双层缓存
5. 内存溢出
问题:缓存数据过多,导致内存溢出
解决方案:
- 使用Redis等外部缓存存储
- 设置合理的缓存大小限制
- 实现缓存淘汰策略
- 定期清理过期缓存
小结
本章节我们学习了NestJS中的缓存机制实现,包括:
- 缓存的基本概念和工作原理
- NestJS缓存模块的使用方法
- 不同的缓存策略和实现
- 缓存失效的处理方法
- Redis集成作为缓存存储
- 自定义缓存拦截器和键生成器
- 缓存的最佳实践和常见问题解决方案
通过这些知识,你可以构建高性能、可靠的缓存系统,提高应用的响应速度和可扩展性,同时避免缓存相关的常见陷阱。
互动问答
问题:缓存的主要作用是什么?
答案:缓存的主要作用是提高应用性能和减少对底层资源的访问,通过存储临时数据来加速数据访问速度。问题:NestJS中如何使用缓存?
答案:可以通过以下方式使用缓存:1. 使用@UseInterceptors(CacheInterceptor)装饰器;2. 使用@CacheKey和@CacheTTL装饰器自定义缓存行为;3. 直接注入CacheManager使用编程式缓存。问题:什么是缓存失效?如何处理缓存失效?
答案:缓存失效是指当数据源发生变化时,确保缓存中的数据也相应更新的过程。处理缓存失效的方法包括:设置TTL、主动删除缓存、使用版本控制等。问题:为什么要使用Redis作为缓存存储?
答案:Redis作为缓存存储的优势包括:高性能、支持持久化、丰富的数据结构、分布式支持、发布/订阅机制等。问题:常见的缓存问题有哪些?如何解决?
答案:常见的缓存问题包括:缓存一致性、缓存穿透、缓存雪崩、缓存击穿、内存溢出等。解决方案包括:主动缓存失效、布隆过滤器、随机TTL、互斥锁、使用Redis等外部缓存存储等。
实践作业
作业1:实现一个基于Redis的缓存系统,支持用户会话存储
作业2:实现缓存预热功能,在应用启动时加载热点数据到缓存
作业3:实现缓存监控,记录缓存命中率和性能指标
作业4:实现缓存降级机制,当Redis不可用时自动切换到内存缓存
作业5:构建一个完整的电商API,使用缓存优化产品列表和详情页的性能
通过完成这些作业,你将能够更加深入地理解缓存机制的实现细节,为构建高性能的应用打下坚实的基础。