AI 生成的摘要
这篇文章详细介绍了如何在NestJS应用中集成Redis缓存,以提高接口的性能和效率。文章首先列出了所需的依赖项,然后通过代码示例展示了在`app.module.ts`中配置Redis缓存的方法。接下来,文章提供了实现自定义缓存拦截器`CustomCacheInterceptor`的步骤,该拦截器允许仅缓存GET请求并根据自定义元数据跳过缓存操作。此外,文章还展示了通过自定义装饰器`@CustomCache`灵活地控制哪些接口需要缓存。 为便于缓存管理,文章介绍了一个缓存服务`HelperCacheService`,该服务支持获取、清除以及按前缀管理缓存内容,以确保在数据更新时能自动删除相关缓存。最后,文章通过实际示例说明了如何在控制器和服务中应用这些缓存策略。整体而言,文章提供了完整且实用的解决方案,帮助开发者在NestJS应用中实现高效的缓存管理。

依赖

package.json
json
    "@nestjs/cache-manager": "^3.0.1",
    "cache-manager": "^6.4.3",
    "@keyv/redis": "^4.4.0",

集成

app.module.ts中注册,这里使用redis作为缓存的存储。

app.module.ts
ts
import { createKeyv } from '@keyv/redis'
import { CacheInterceptor, CacheModule } from '@nestjs/cache-manager'
import { Module } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { APP_INTERCEPTOR } from '@nestjs/core'

import { AppController } from './app.controller'
import { RedisModule } from './common/redis/redis.module'

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    RedisModule,
    CacheModule.registerAsync({
      useFactory: async (configService: ConfigService) => {
        const host = configService.get('REDIS_HOST') || '127.0.0.1'
        const port = configService.get('REDIS_PORT') || 6379
        const password = configService.get('REDIS_PASSWORD')
        return {
          stores: [
            createKeyv(
              {
                url: `redis://${host}:${port}`,
                password,
              },
              {
                namespace:
                  configService.get('CACHE_NAMESPACE') || 'request_cache_',
              },
            ),
          ],
          ttl: configService.get('CACHE_TTL') || 60000,
        }
      },
      isGlobal: true,
      inject: [ConfigService],
    }),
  ],
  controllers: [AppController],
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: CacheInterceptor,
    },
  ],
})
export class AppModule {}

这样缓存就会在全局生效

然后实现部分接口不进行缓存,以及修改某些数据后,删除已经缓存的部分

skipCache

可以基于CacheInterceptor 拓展一个CustomCacheInterceptor来进行过滤

custom-cache.interceptor.ts
ts
import { CacheInterceptor } from '@nestjs/cache-manager'
import { ExecutionContext, Injectable, Logger } from '@nestjs/common'

@Injectable()
export class CustomCacheInterceptor extends CacheInterceptor {
  private logger = new Logger('CustomCacheInterceptor')

  trackBy(context: ExecutionContext): string | undefined {
    try {
      const request = context.switchToHttp().getRequest()
      // 只缓存 GET 请求
      if (request.method !== 'GET') {
        return undefined
      }
      const handler = context.getHandler()
      const shouldSkipCache = Reflect.getMetadata('skipCache', handler) === true
      if (shouldSkipCache) {
        return undefined
      }

      const url = request.url
      return url
    } catch (error) {
      this.logger.error(error)
      return undefined
    }
  }
}

这里实现了只缓存 GET请求,并通过Reflect.getMetadata来获取skipCacheshipCachetrue则跳过缓存。

skipCache的设置可以自定义一个装饰器来做

cache.decorator.ts
ts
import { applyDecorators, SetMetadata } from '@nestjs/common'

export function CustomCache(skipCache: boolean = false) {
  applyDecorators(SetMetadata('skipCache', skipCache))
}

在不需要缓存的controller中设置

删除已经缓存的部分

在这之前我们先拓展 cache.decorator.tscustom-cache.interceptor.ts来支持自定义前缀,方便后面删除

custom-cache.interceptor.ts
ts
import { CacheInterceptor } from '@nestjs/cache-manager'
import { ExecutionContext, Injectable, Logger } from '@nestjs/common'

@Injectable()
export class CustomCacheInterceptor extends CacheInterceptor {
  private logger = new Logger('CustomCacheInterceptor')

  trackBy(context: ExecutionContext): string | undefined {
    try {
      const request = context.switchToHttp().getRequest()
      // 只缓存 GET 请求
      if (request.method !== 'GET') {
        return undefined
      }
      const handler = context.getHandler()
      const shouldSkipCache = Reflect.getMetadata('skipCache', handler) === true
      if (shouldSkipCache) {
        return undefined
      }

      const customCacheKey =
        Reflect.getMetadata('customCacheKey', handler) || ''

      const url = request.url
      return `${customCacheKey}:${url}`
    } catch (error) {
      this.logger.error(error)
      return undefined
    }
  }
}

这样修改后,controller 就可以这样:

这样就可以根据custormKey来批量删除

实现一个cache.service.ts来查询、删除缓存:

helper.cache.service.ts
ts
import { Cache } from 'cache-manager'

import { CACHE_MANAGER } from '@nestjs/cache-manager'
import { Inject, Injectable, Logger } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'

@Injectable()
export class HelperCacheService {
  @Inject(CACHE_MANAGER)
  private cacheManager: Cache

  @Inject(ConfigService)
  private configService: ConfigService

  private logger = new Logger('HelperCacheService')
  async getCacheAllKeys(): Promise<string[]> {
    const keys: string[] = []
    try {
      const val = await this.cacheManager.stores[0].iterator(
        this.configService.get('CACHE_NAMESPACE') || 'request_cache_',
      )
      for await (const [key] of val) {
        keys.push(key)
      }
      return keys
    } catch (error) {
      this.logger.error('Cache iteration error:', error)
      return []
    }
  }

  async clearCache() {
    const keys = await this.getCacheAllKeys()
    await this.clearCacheByKeys(keys)
  }

  async clearCacheByPrefix(prefix: string) {
    const keys = await this.getCacheAllKeys()
    const cacheKeys = keys.filter((key) => key.startsWith(prefix))
    await this.clearCacheByKeys(cacheKeys)
  }

  async clearCacheByKeys(keys: string[]) {
    if (keys.length === 0) return

    try {
      await Promise.all(
        keys.map((key) => this.cacheManager.stores[0].delete(key)),
      )
      this.logger.log(`Cleared ${keys.length} cache entries`)
    } catch (error) {
      this.logger.error('Failed to clear cache by keys:', error)
    }
  }

  async getCacheByKeys(keys: string[]) {
    const values: any[] = []
    for (const key of keys) {
      const value = await this.cacheManager.stores[0].get(key)
      values.push(value)
    }
    return values
  }

  async getCacheByPrefix(prefix: string) {
    const keys = await this.getCacheAllKeys()
    const values: any[] = []
    for (const key of keys) {
      if (key.startsWith(prefix)) {
        const value = await this.cacheManager.stores[0].get(key)
        values.push(value)
      }
    }
    return values
  }
}
使用 this.cacheManager.stores[0].iterator时,应当注意redis版本不能过低。实测redis:5-alpine 不行,redis:7-alpine可以

使用示例:

post.controller.ts
ts
import { IncomingMessage } from 'node:http'
import { FastifyRequest } from 'fastify'

import { Controller, Get, Param, Query, Req } from '@nestjs/common'

import { CustomCache } from '~/common/decorators/cache.decorator'

import { PostFindAllDto, PostPageDto } from './post.dto'
import { PostService } from './post.service'

@Controller('post')
export class PostController {
  constructor(private readonly postService: PostService) {}

  @Get()
  @CustomCache('post-list')
  getList(@Query() query: PostPageDto) {
    return this.postService.getList(query)
  }

  @Get('findAll')
  @CustomCache('post-findAll')
  findAll(@Query() query: PostFindAllDto) {
    return this.postService.findAll(query)
  }

  @Get(':id')
  getDetail(@Param('id') id: string, @Req() req: IncomingMessage) {
    return this.postService.getDetail(id, req)
  }

  @Get(':id/read')
  @CustomCache(false)
  async incrementReadCount(
    @Param('id') id: string,
    @Req() req: FastifyRequest,
  ) {
    return await this.postService.incrementReadCount(id, req)
  }
}