nestjs 缓存配置及防抖拦截器
1、编写全局拦截器,
2、配置缓存服务,以便于依赖注入
3、编写添加元数据方法,后面防抖拦截器是否需要拦截做准备
4、编写全局拦截器,依赖注入缓存service,在拦截器中每次进入的时候从缓存中读取,如果从在,则抛异常,否则存储在缓存中
5、将拦截器全局引入
1、下载安装
pnpm i keyv @keyv/redis cache-manager cacheable
2、配置缓存服务,以便于依赖注入
providers: [{provide: 'CACHE_MANAGER',inject: [ConfigService],useFactory: (configService: ConfigService) => {return createCache({nonBlocking: true,stores: [// 内存中存储new Keyv({store: new CacheableMemory({ ttl: 60000, lruSize: 5000 }),namespace:'',}),// redis中存储new Keyv({store: new KeyvRedis(configService.get('redis.url')),namespace: ''})]})}}
]
exports: ['CACHE_MANAGER'
],
完整全局配置
import { Global, Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { HttpModule } from '@nestjs/axios'; import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; import configuration from '../../config/index'; import { JwtModule } from '@nestjs/jwt'; import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; import { JwtGuard } from 'src/utils/jwt/jwt-guard'; import { JwtStrategy } from 'src/utils/jwt/jwt-strategy'; import { WinstonService } from 'src/utils/logger/winston-service'; import { CatchLoggerFilter } from 'src/utils/logger/catch-logger-filter'; import { ResponseLoggerInterceptor } from 'src/utils/logger/response-logger-interceptor'; import { RedisModule } from '@nestjs-modules/ioredis'; import { RequirePermissionGuard } from 'src/utils/premission/require-premission.guard'; import { DebounceInterceptor } from 'src/utils/debounce/debounce.interceptor'; import { Keyv } from 'keyv'; import KeyvRedis from '@keyv/redis' import { createCache } from 'cache-manager'; import { CacheableMemory } from 'cacheable'@Global() @Module({imports: [ConfigModule.forRoot({isGlobal: true,load: [configuration],}),TypeOrmModule.forRootAsync({name: "default",inject: [ConfigService],useFactory: (configService: ConfigService) => {return {type: 'mysql',...configService.get('db.mysql'),timezone: '+08:00',// logger: 'advanced-console',entities: [__dirname + '/../**/*.entity.{js,ts}'],} as TypeOrmModuleOptions;},}),// TypeOrmModule.forRootAsync({// name: "oracle",// inject: [ConfigService],// useFactory: async (configService: ConfigService) => {// return {// type: 'oracle',// ...configService.get('db.oracle'),// // logger: 'advanced-console',// timezone: '+08:00',// entities: [__dirname + '/../**/*.entity.{js,ts}'],// } as TypeOrmModuleOptions;// },// }),HttpModule.registerAsync({inject: [ConfigService],useFactory: async (configService: ConfigService) => {return {timeout: configService.get('http.timeout'),maxRedirects: configService.get('http.maxRedirects'),};},}),RedisModule.forRootAsync({inject: [ConfigService],useFactory: (configService: ConfigService) => {return {type: "single",url: configService.get('redis.url'),};},}),JwtModule.registerAsync({inject: [ConfigService],global: true,useFactory: (configService: ConfigService) => {return {secret: configService.get('jwt.secretkey'),// signOptions: { expiresIn: configService.get('jwt.expiresin') },};},}),],providers: [JwtStrategy,{provide: APP_GUARD,useFactory: (configService: ConfigService) => {return new JwtGuard(configService);},inject: [ConfigService],},{provide: APP_GUARD,useClass: RequirePermissionGuard},{provide: WinstonService,inject: [ConfigService],useFactory: (configService: ConfigService) => {return new WinstonService(configService);}},{provide: APP_FILTER,useClass: CatchLoggerFilter},{provide: APP_INTERCEPTOR,useClass: ResponseLoggerInterceptor},{provide: APP_INTERCEPTOR,useClass: DebounceInterceptor},{provide: 'CACHE_MANAGER',inject: [ConfigService],useFactory: (configService: ConfigService) => {return createCache({nonBlocking: true,stores: [// 内存中存储new Keyv({store: new CacheableMemory({ ttl: 60000, lruSize: 5000 }),namespace:'',}),// redis中存储new Keyv({store: new KeyvRedis(configService.get('redis.url')),namespace: ''})]})}},],exports: [WinstonService,HttpModule,'CACHE_MANAGER'], }) export class ShareModule { }
3、编写添加元数据方法,为后面防抖拦截器是否需要拦截做准备
import { SetMetadata } from '@nestjs/common';
export const Debounce = (keyPattern: string, ttl: number = 5) => SetMetadata('debounce', { keyPattern, ttl });
4、编写防抖拦截器
import { CallHandler, ExecutionContext, HttpException, Inject, Injectable, NestInterceptor } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Observable } from "rxjs";
import type { Cache } from 'cache-manager'
import { CacheEnum } from "../base-enum";
@Injectable()
export class DebounceInterceptor implements NestInterceptor {constructor(private reflector: Reflector,@Inject('CACHE_MANAGER') private cache: Cache,) {}async intercept(context: ExecutionContext, next: CallHandler<any>): Promise<Observable<any>> {// 判断是否有元数据const debounce = this.reflector.getAllAndOverride('debounce', [context.getClass(),context.getHandler()]);// 如果没有 说明不需要控制if (!debounce) {return next.handle();}const { keyPattern, ttl } = debounce;const request = context.switchToHttp().getRequest();const cacheKey = CacheEnum.DEBOUNCE_KEY + this.resolveKey(keyPattern, request);const isBlocked = await this.cache.get(cacheKey);if (isBlocked) {throw new HttpException('操作过于频繁,请稍后再试', 429);}const data = await this.cache.set(cacheKey, true, ttl);return next.handle();}private resolveKey(pattern: string, request: any): string {return pattern.replace(/\{(\w+)\}/g, (match, paramName) => {// 优先从 params、body、query、user 中查找const sources = [request.params, request.user, request.body, request.query];for (const src of sources) {if (src && src[paramName] !== undefined) {return src[paramName];}}// 支持 user.id 等if (paramName.includes('.')) {const parts = paramName.split('.');let val = request;for (const part of parts) {val = val?.[part];}return val || 'unknown';}return 'unknown';});}
}
5、全局引入
providers: [{provide: APP_INTERCEPTOR,useClass: DebounceInterceptor},
]
6、使用
需要做防抖的控制器上添加元数据. @Debounce(标识,过期时间-毫秒)
@Put("/update") @Debounce('userUpdate:{userId}', 5000) update(@Body() body: UpdateUserDto) {return this.userService.updateUser(body.userId, body) }
---------------------------------------------------------------------------------------------------------------------------------
既然自己定义了缓存服务,那么全局注册也写一个好了,但是不建议全局化哈
基于上面全局注册的"CACHE_MANAGER_INSTANCE" service,再写一个拦截器,拦截器中只处理get请求,同样的原理,如果是get请求的话 ,从元数据中取值,先去缓存中查找,存在直接返回,不存在走自己的方法,完事儿后再像缓存中保存一份
import {CallHandler,ExecutionContext,Inject,Injectable,NestInterceptor, } from '@nestjs/common'; import { Observable, of } from 'rxjs'; import { tap } from 'rxjs/operators'; import { CACHE_KEY_METADATA, CACHE_TTL_METADATA, CacheKey, CacheTTL } from '@nestjs/cache-manager'; import type { Cache } from 'cache-manager';@Injectable() export class HttpCacheInterceptor implements NestInterceptor {constructor(@Inject("CACHE_MANAGER_INSTANCE") private readonly cacheManager: Cache,) { }async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {const request = context.switchToHttp().getRequest();if (request.method !== 'GET') return next.handle();const key = this.getCacheKey(context);const ttl = this.getTTL(context);const cached = await this.cacheManager.get(key);if (cached) return of(cached);return next.handle().pipe(tap((response) => {this.cacheManager.set(key, response, ttl);}),);}private getCacheKey(context: ExecutionContext): string {const key = Reflect.getMetadata(CACHE_KEY_METADATA, context.getHandler());const request = context.switchToHttp().getRequest();return key || `${request.method}_${request.url}`;}private getTTL(context: ExecutionContext): number {const ttl = Reflect.getMetadata(CACHE_TTL_METADATA, context.getHandler());return ttl || 60;} }
然后全局化就可以啦
{provide: APP_INTERCEPTOR,useClass: HttpCacheInterceptor }
使用
/** * * 因为全局拦截器中取的是 import { CACHE_KEY_METADATA, CACHE_TTL_METADATA } from '@nestjs/cache-manager'; * 所以这里直接使用原有的就好了 CacheKey 和 CacheTTL */ @Get("/list") @CacheTTL(10000) //不指定的话取全局默认是时间 @CacheKey('list') //不指定的话取路由类型+路径地址 @RequirePermission(['system:user:query']) findList(@Query() query: ListUserDto) {return this.userService.paginateUser(query) }
单个方法使用手动写入一下就好了
// 🔍 先查缓存 const cached = await this.cacheManager.get(‘自己定义个key’); if (cached) {console.log(`🎯 缓存命中: ${cacheKey}`);return cached; }// 🚀 查询数据库(模拟) const result = await this.queryFromDatabase(query);// 💾 写入缓存,毫秒为单位 await this.cacheManager.set(cacheKey, result, 10000);return result;