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;