首页 / NestJS 入门教程 / 缓存

NestJS 入门教程

缓存

本教程共 47 篇 · 第 30 篇 · 更新于 2026-08-09 · 约 11 分钟阅读

NestJS缓存CacheModuleRedisCacheInterceptor性能优化

本节目标:掌握 NestJS 中的缓存技术,包括内存缓存、自动缓存响应、Redis 集成和缓存策略配置。

有些接口特别耗性能。查数据库、调第三方 API、跑复杂计算,每次都从头来一遍,用户等得急,服务器也扛不住。

缓存就是解决这个问题的。把算好的结果存起来,下次直接取,不用重新算。就像你把常用电话号码存到通讯录,不用每次都翻电话簿。

NestJS 提供了开箱即用的缓存方案。

安装依赖

npm install @nestjs/cache-manager cache-manager

默认是内存缓存,数据存在应用内存里。如果想用 Redis,还得装额外的包,后面会讲。

基础用法

启用缓存模块

在根模块里导入 CacheModule

import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';

@Module({
  imports: [CacheModule.register()],
})
export class AppModule {}

这样缓存就启用了,默认配置是内存存储,永不过期。

手动操作缓存

注入 CACHE_MANAGER 就能用了:

import { Injectable, Inject } from '@nestjs/common';
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';

@Injectable()
export class AppService {
  constructor(
    @Inject(CACHE_MANAGER) 
    private cacheManager: Cache,
  ) {}

  async getData() {
    // 先查缓存
    const cached = await this.cacheManager.get('my-key');
    if (cached) {
      return cached;
    }

    // 缓存没有,去查数据库
    const data = await this.fetchFromDatabase();
    
    // 存到缓存,设置 60 秒过期
    await this.cacheManager.set('my-key', data, 60000);
    
    return data;
  }

  async clearCache() {
    // 删除某个缓存
    await this.cacheManager.del('my-key');
    
    // 清空所有缓存
    await this.cacheManager.clear();
  }
}
Note

get() 方法在缓存不存在时返回 undefined(v6 及之前版本返回 null)。判断的时候用 falsy 就行。

设置过期时间

TTL(Time To Live)就是缓存的保质期,单位是毫秒:

// 60 秒后过期
await this.cacheManager.set('key', 'value', 60000);

// 永不过期
await this.cacheManager.set('key', 'value', 0);

自动缓存响应

每次手动 get/set 太麻烦。NestJS 提供了 CacheInterceptor,一行代码搞定接口缓存。

单个接口缓存

import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { CacheInterceptor } from '@nestjs/cache-manager';

@Controller('users')
@UseInterceptors(CacheInterceptor)
export class UsersController {
  @Get()
  findAll() {
    // 这个接口的响应会被自动缓存
    return this.usersService.findAll();
  }
}
Warning

只有 GET 请求会被缓存。用了 @Res() 的接口不能用缓存拦截器。

全局缓存

给所有 GET 接口都加上缓存:

import { Module } from '@nestjs/common';
import { CacheModule, CacheInterceptor } from '@nestjs/cache-manager';
import { APP_INTERCEPTOR } from '@nestjs/core';

@Module({
  imports: [CacheModule.register()],
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: CacheInterceptor,
    },
  ],
})
export class AppModule {}

这样所有 GET 接口的响应都会被缓存。

自定义缓存策略

设置全局 TTL

CacheModule.register({
  ttl: 5000, // 全局 5 秒过期
});

自定义缓存键和 TTL

有时候需要更细粒度的控制:

import { Controller, Get } from '@nestjs/common';
import { CacheKey, CacheTTL } from '@nestjs/cache-manager';

@Controller('users')
export class UsersController {
  @Get()
  @CacheKey('users-list')
  @CacheTTL(20000) // 20 秒过期
  findAll() {
    return this.usersService.findAll();
  }
}

也可以在控制器级别设置 TTL:

@Controller('users')
@CacheTTL(5000) // 整个控制器 5 秒
export class UsersController {
  @Get()
  @CacheTTL(20000) // 这个方法 20 秒,优先级更高
  findAll() {
    return [];
  }
}
Tip

方法级别的 @CacheTTL() 会覆盖控制器级别的设置。

自定义缓存键生成规则

默认情况下,缓存键是请求的 URL。但有时候你想根据其他因素来区分,比如用户身份:

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

@Injectable()
class CustomCacheInterceptor extends CacheInterceptor {
  trackBy(context: ExecutionContext): string | undefined {
    const request = context.switchToHttp().getRequest();
    const userId = request.user?.id;
    
    // 不同用户缓存不同
    return userId 
      ? `${request.url}:user:${userId}` 
      : request.url;
  }
}

WebSockets 和微服务

缓存拦截器也能用在 WebSocket 和微服务里:

@CacheKey('events')
@UseInterceptors(CacheInterceptor)
@SubscribeMessage('events')
handleEvent(client: Client, data: string[]): Observable<string[]> {
  return [];
}
Note

WebSocket 和微服务里必须用 @CacheKey() 指定缓存键,不然没法缓存。

使用 Redis

内存缓存有个问题:重启应用缓存就没了。生产环境通常用 Redis。

安装 Redis 驱动

npm install @keyv/redis

配置 Redis

import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import KeyvRedis from '@keyv/redis';
import { Keyv } from 'keyv';
import { KeyvCacheableMemory } from 'cacheable';

@Module({
  imports: [
    CacheModule.registerAsync({
      useFactory: async () => {
        return {
          stores: [
            // 内存缓存作为第一层
            new Keyv({
              store: new KeyvCacheableMemory({ 
                ttl: 60000, 
                lruSize: 5000 
              }),
            }),
            // Redis 作为第二层
            new KeyvRedis('redis://localhost:6379'),
          ],
        };
      },
    }),
  ],
})
export class AppModule {}

这种多层缓存的策略很常见。内存缓存速度快,Redis 容量大。先查内存,没有再查 Redis。

异步配置

从配置文件或环境变量读取 Redis 地址:

CacheModule.registerAsync({
  imports: [ConfigModule],
  useFactory: async (configService: ConfigService) => ({
    stores: [
      new KeyvRedis(configService.get('REDIS_URL')),
    ],
  }),
  inject: [ConfigService],
})

全局模块

不想每个模块都导入 CacheModule?设为全局模块:

CacheModule.register({
  isGlobal: true,
});

这样在根模块导入一次,其他地方就能直接用了。

踩坑经验

坑1:缓存了不该缓存的东西

不是所有接口都适合缓存。涉及写操作的接口、实时性要求高的数据,千万别缓存。

坑2:缓存雪崩

大量缓存同时过期,请求全部打到数据库,服务器直接挂掉。解决办法是给 TTL 加个随机值:

const ttl = 60000 + Math.random() * 10000; // 60-70秒随机
await this.cacheManager.set('key', 'value', ttl);

坑3:缓存穿透

查询一个不存在的数据,每次都会穿透到数据库。可以用布隆过滤器或者缓存空值来解决。

坑4:内存泄漏

内存缓存如果不设置 TTL,数据会越来越多,最终内存溢出。一定要设置合理的过期时间。

坑5:缓存一致性

数据更新了,缓存没更新,用户看到的还是旧数据。常见策略:

  • 写操作时删除相关缓存
  • 设置较短的 TTL
  • 使用缓存失效通知机制
async updateUser(id: number, data: UpdateUserDto) {
  // 更新数据库
  await this.db.update(id, data);
  
  // 删除缓存
  await this.cacheManager.del(`user:${id}`);
}

小结

缓存是提升性能的利器。关键知识点回顾:

  • CacheModule.register() 启用缓存
  • CACHE_MANAGER 注入手动操作
  • CacheInterceptor 自动缓存响应
  • @CacheKey()@CacheTTL() 自定义策略
  • 生产环境用 Redis 做持久化缓存

缓存虽好,但别滥用。想清楚哪些数据适合缓存、缓存多久、怎么失效,这些比会用 API 更重要。

下一章是教程的最后一章,我们聊聊定时任务。