nestjs 缓存配置及防抖拦截器

发布于:2025-09-06 ⋅ 阅读:(14) ⋅ 点赞:(0)

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;


网站公告

今日签到

点亮在社区的每一天
去签到