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
tsimport { 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
tsimport { 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
来获取skipCache
,
shipCache
为 true
则跳过缓存。
skipCache
的设置可以自定义一个装饰器来做
cache.decorator.ts
tsimport { applyDecorators, SetMetadata } from '@nestjs/common'
export function CustomCache(skipCache: boolean = false) {
applyDecorators(SetMetadata('skipCache', skipCache))
}
在不需要缓存的controller
中设置
删除已经缓存的部分
在这之前我们先拓展 cache.decorator.ts
和 custom-cache.interceptor.ts
来支持自定义前缀,方便后面删除
custom-cache.interceptor.ts
tsimport { 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
tsimport { 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
tsimport { 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)
}
}