当前位置: 首页 > web >正文

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;

http://www.xdnf.cn/news/20286.html

相关文章:

  • C# 阿里云 OSS 图片上传步骤及浏览器查看方法
  • 深入解析汇编语言的奥秘
  • 文件不展示Eslint的报错红色
  • 前端三件套+springboot后端连通尝试
  • 系统学习算法 专题十八 队列+宽搜
  • Doris 数据仓库例子
  • OpenCV C++ 色彩空间详解:转换、应用与 LUT 技术
  • 一文详解深度学习中神经网络的各层结构与功能!
  • SQL-DML
  • 计算机网络4 第四章 网络层——网络间的通信问题(省际之间如何规划信件运输路线)
  • 酒店实习生转正信息调整编程实现(Python字典应用基础题)
  • 【yolo】YOLOv8 训练模型参数与多机环境差异总结
  • Kafka面试精讲 Day 8:日志清理与数据保留策略
  • Grafana 导入仪表盘失败:从日志排查到解决 max\_allowed\_packet 问题
  • 汽车软件研发智能化:AI在CI/CD中的实践
  • 实践指南:利用衡石AI Data Agent实现自然语言驱动的指标开发与归因
  • 【最新版】发烧级完美解码播放器PureCodec v2025.08.29 中文免费版_电脑播放器影音解码包
  • 基于51单片机WIFI智能家居系统设计
  • 相机刮除拜尔阵列
  • 使用海康机器人相机SDK实现基本参数配置(C语言示例)
  • Linux查看相机支持帧率和格式
  • Linux系统安全加固:构建云计算安全的第一道防线
  • 迁移学习-ResNet
  • VBA 中使用 ADODB 操作 SQLite 插入中文乱码问题
  • JVM新生代和老生代比例如何设置?
  • Vue 3 项目中引入 Iconify
  • Spring Boot 和 Spring Cloud: 区别与联系
  • Oracle到ClickHouse:异构数据库ETL的坑与解法
  • HTML 各种事件的使用说明书
  • Spring Boot AOP:优雅解耦业务与非业务逻辑的利器