Techniques

缓存

学习如何在 NestJS 中使用缓存来提升应用程序性能,包括内存缓存、Redis 缓存、自动缓存响应和自定义缓存策略。

缓存

缓存是一种强大而直接的技术,用于增强应用程序的性能。通过充当临时存储层,它允许更快地访问经常使用的数据,减少了重复获取或计算相同信息的需要。这导致更快的响应时间和改进的整体效率。

安装

要在 Nest 中开始使用缓存,您需要安装 @nestjs/cache-manager 包以及 cache-manager 包。

$ npm install @nestjs/cache-manager cache-manager

默认情况下,所有内容都存储在内存中;由于 cache-manager 在底层使用 Keyv,您可以通过安装适当的包轻松切换到更高级的存储解决方案,例如 Redis。我们稍后会更详细地介绍这一点。

内存缓存

要在应用程序中启用缓存,请导入 CacheModule 并使用 register() 方法配置它:

import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { AppController } from './app.controller';

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

此设置使用默认设置初始化内存缓存,允许您立即开始缓存数据。

与缓存存储交互

要与缓存管理器实例交互,请使用 CACHE_MANAGER 令牌将其注入到您的类中,如下所示:

constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}

提示 Cache 类和 CACHE_MANAGER 令牌都从 @nestjs/cache-manager 包中导入。

Cache 实例上的 get 方法(来自 cache-manager 包)用于从缓存中检索项目。如果项目在缓存中不存在,将返回 null

const value = await this.cacheManager.get('key');

要向缓存添加项目,请使用 set 方法:

await this.cacheManager.set('key', 'value');

注意 内存缓存存储只能存储结构化克隆算法支持的类型的值。

您可以手动为此特定键指定 TTL(以毫秒为单位的过期时间),如下所示:

await this.cacheManager.set('key', 'value', 1000);

其中 1000 是以毫秒为单位的 TTL - 在这种情况下,缓存项目将在一秒后过期。

要禁用缓存的过期,请将 ttl 配置属性设置为 0

await this.cacheManager.set('key', 'value', 0);

要从缓存中删除项目,请使用 del 方法:

await this.cacheManager.del('key');

要清除整个缓存,请使用 clear 方法:

await this.cacheManager.clear();

自动缓存响应

警告GraphQL 应用程序中,拦截器为每个字段解析器单独执行。因此,CacheModule(使用拦截器缓存响应)将无法正常工作。

要启用自动缓存响应,只需在您想要缓存数据的地方绑定 CacheInterceptor

@Controller()
@UseInterceptors(CacheInterceptor)
export class AppController {
  @Get()
  findAll(): string[] {
    return [];
  }
}

警告 只有 GET 端点被缓存。此外,注入原生响应对象(@Res())的 HTTP 服务器路由不能使用缓存拦截器。有关更多详细信息,请参阅响应映射

要减少所需的样板代码量,您可以将 CacheInterceptor 全局绑定到所有端点:

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

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

生存时间(TTL)

ttl 的默认值是 0,意味着缓存永不过期。要指定自定义 TTL,您可以在 register() 方法中提供 ttl 选项,如下所示:

CacheModule.register({
  ttl: 5000, // 毫秒
});

全局使用模块

当您想在其他模块中使用 CacheModule 时,您需要导入它(这是任何 Nest 模块的标准做法)。或者,通过将选项对象的 isGlobal 属性设置为 true 来将其声明为全局模块,如下所示。在这种情况下,一旦在根模块(例如 AppModule)中加载,您就不需要在其他模块中导入 CacheModule

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

全局缓存覆盖

虽然启用了全局缓存,但缓存条目存储在基于路由路径自动生成的 CacheKey 下。您可以在每个方法的基础上覆盖某些缓存设置(@CacheKey()@CacheTTL()),允许为单个控制器方法自定义缓存策略。这在使用不同的缓存存储时可能最相关。

您可以在每个控制器的基础上应用 @CacheTTL() 装饰器来为整个控制器设置缓存 TTL。在同时定义控制器级别和方法级别缓存 TTL 设置的情况下,在方法级别指定的缓存 TTL 设置将优先于在控制器级别设置的设置。

@Controller()
@CacheTTL(50)
export class AppController {
  @CacheKey('custom_key')
  @CacheTTL(20)
  findAll(): string[] {
    return [];
  }
}

提示 @CacheKey()@CacheTTL() 装饰器从 @nestjs/cache-manager 包中导入。

@CacheKey() 装饰器可以与或不与相应的 @CacheTTL() 装饰器一起使用,反之亦然。可以选择仅覆盖 @CacheKey() 或仅覆盖 @CacheTTL()。未使用装饰器覆盖的设置将使用全局注册的默认值(请参阅自定义缓存)。

WebSockets 和微服务

您还可以将 CacheInterceptor 应用于 WebSocket 订阅者以及微服务的模式(无论使用何种传输方法)。

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

但是,需要额外的 @CacheKey() 装饰器来指定用于随后存储和检索缓存数据的键。另外,请注意您不应该缓存所有内容。执行某些业务操作而不是简单查询数据的操作永远不应该被缓存。

此外,您可以使用 @CacheTTL() 装饰器指定缓存过期时间(TTL),这将覆盖全局默认 TTL 值。

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

提示 @CacheTTL() 装饰器可以与或不与相应的 @CacheKey() 装饰器一起使用。

调整跟踪

默认情况下,Nest 使用请求 URL(在 HTTP 应用程序中)或缓存键(在 websockets 和微服务应用程序中,通过 @CacheKey() 装饰器设置)来将缓存记录与您的端点关联。然而,有时您可能希望基于不同的因素设置跟踪,例如,使用 HTTP 标头(例如 Authorization 来正确识别 profile 端点)。

为了实现这一点,创建 CacheInterceptor 的子类并覆盖 trackBy() 方法。

@Injectable()
class HttpCacheInterceptor extends CacheInterceptor {
  trackBy(context: ExecutionContext): string | undefined {
    return 'key';
  }
}

使用替代缓存存储

切换到不同的缓存存储很简单。首先,安装适当的包。例如,要使用 Redis,请安装 @keyv/redis 包:

$ npm install @keyv/redis

有了这个,您可以使用多个存储注册 CacheModule,如下所示:

import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { AppController } from './app.controller';
import KeyvRedis from '@keyv/redis';
import { Keyv } from 'keyv';
import { CacheableMemory } from 'cacheable';

@Module({
  imports: [
    CacheModule.registerAsync({
      useFactory: async () => {
        return {
          stores: [
            new Keyv({
              store: new CacheableMemory({ ttl: 60000, lruSize: 5000 }),
            }),
            new KeyvRedis('redis://localhost:6379'),
          ],
        };
      },
    }),
  ],
  controllers: [AppController],
})
export class AppModule {}

在此示例中,我们注册了两个存储:CacheableMemoryKeyvRedisCacheableMemory 存储是一个简单的内存存储,而 KeyvRedis 是一个 Redis 存储。stores 数组用于指定您要使用的存储。数组中的第一个存储是默认存储,其余的是回退存储。

查看 Keyv 文档以获取有关可用存储的更多信息。

异步配置

您可能希望异步传递模块选项,而不是在编译时静态传递它们。在这种情况下,使用 registerAsync() 方法,它提供了几种处理异步配置的方法。

一种方法是使用工厂函数:

CacheModule.registerAsync({
  useFactory: () => ({
    ttl: 5,
  }),
});

我们的工厂表现得像所有其他异步模块工厂(它可以是 async 并且能够通过 inject 注入依赖项)。

CacheModule.registerAsync({
  imports: [ConfigModule],
  useFactory: async (configService: ConfigService) => ({
    ttl: configService.get('CACHE_TTL'),
  }),
  inject: [ConfigService],
});

或者,您可以使用 useClass 方法:

CacheModule.registerAsync({
  useClass: CacheConfigService,
});

上述构造将在 CacheModule 内实例化 CacheConfigService 并将使用它来获取选项对象。CacheConfigService 必须实现 CacheOptionsFactory 接口以提供配置选项:

@Injectable()
class CacheConfigService implements CacheOptionsFactory {
  createCacheOptions(): CacheModuleOptions {
    return {
      ttl: 5,
    };
  }
}

如果您希望使用从不同模块导入的现有配置提供程序,请使用 useExisting 语法:

CacheModule.registerAsync({
  imports: [ConfigModule],
  useExisting: ConfigService,
});

这与 useClass 的工作方式相同,但有一个关键区别 - CacheModule 将查找导入的模块以重用任何已创建的 ConfigService,而不是实例化自己的。

提示 CacheModule#registerCacheModule#registerAsyncCacheOptionsFactory 有一个可选的泛型(类型参数)来缩小特定于存储的配置选项,使其类型安全。

您还可以将所谓的 extraProviders 传递给 registerAsync() 方法。这些提供程序将与模块提供程序合并。

CacheModule.registerAsync({
  imports: [ConfigModule],
  useClass: ConfigService,
  extraProviders: [MyAdditionalProvider],
});

当您想要为工厂函数或类构造函数提供额外的依赖项时,这很有用。

示例

可以在这里找到一个工作示例。