您的位置:首页 > 财经 > 产业 > 如何建立公司网址_百度推广信息流有用吗_怎么优化百度关键词_磁力猫搜索引擎入口官网

如何建立公司网址_百度推广信息流有用吗_怎么优化百度关键词_磁力猫搜索引擎入口官网

2024/12/29 7:17:24 来源:https://blog.csdn.net/sharkchen111/article/details/144682807  浏览:    关键词:如何建立公司网址_百度推广信息流有用吗_怎么优化百度关键词_磁力猫搜索引擎入口官网
如何建立公司网址_百度推广信息流有用吗_怎么优化百度关键词_磁力猫搜索引擎入口官网

nestjs:GET REQUEST 缓存

背景

项目使用nestjs作为服务端开发框架,使用了内置的缓存功能。项目内有很多功能都是清单类的,清单列表在编辑或删除后,通过请求刷新列表信息时,会出现获取的数据与为修改前的数据一致的情况。
具体现象:
待办模块中有获取待办列表的接口:/todo/get-list,删除待办接口:/todo/delete-todo
在删除待办的场景中,前端先请求/todo/delete-todo删除指定待办,接口返回成功后请求/todo/get-list刷新待办列表。这时候就会出现列表不更新的情况。

原因分析

第一时间我以为是删除的逻辑有问题,查看代码没问题,调试删除接口,执行没问题,包括数据库中的数据也是删除成功,删除的接口没问题。
接口没问题,那么会不会是前端列表更新的问题?删除成功获取到新列表数据后,页面数据没更新导致的?这个也很快就排除,数据获取没问题,页面更新没问题。
那就剩下列表接口的问题,在进入页面时,第一次请求列表接口,返回5条待办数据,然后删除最后一条待办,再次请求列表接口,还是返回5条待办数据。wtf。。。

进一步分析

首先判断列表数据查询是否有问题,因为删除是软删除,先排除查询条件错误的问题。查询条件并没有问题。
排除查询条件问题后,接下来排除是否是返回数据处理逻辑的问题,然而,数据处理逻辑只是做了简单的加工,没有进一步查询补充数据的操作。
到这里我有点懵,没见过这种情况,现在能确定的肯定是接口出问题,而且是偶发的,因为添加操作后也会刷新列表的请求,但是一直没出现这个问题。
只能从入口排查,于是,我在controller打上断点,然而,删除后的第二次列表接口请求并没有加进入到controller中。

在这里插入图片描述

看下nestjs的请求的执行流程:
在这里插入图片描述

既然没有进入controller,那肯定就是在前面某一个节点提前响应了请求,我的项目中只使用了拦截器和管道,在两个地方打上断点,结果,拦截器正常执行,但是管道没有被执行。OK,可以确定是拦截器的问题,但是项目中的自定义拦截器并没有对正常的请求直接返回的操作。过一遍项目的代码,发现有一个非自定义的拦截器:

@Module({imports: [],controllers: [],providers: [{provide: APP_INTERCEPTOR,useClass: CacheInterceptor},
});

这是在全局注入的缓存功能,然后在官网上看到这样的描述:
在这里插入图片描述

引入了缓存后,默认会缓存GET请求。

GET REQUEST 缓存

GET请求缓存这个行为,主要是为了减少短时间内大量相同的请求对服务端的负荷。那么nestjs具体是怎么去做这个缓存的,存储的规则是什么,如果不需要这个缓存,要怎么处理呢?
直接看CacheInterceptor的源码:

 async intercept(context: ExecutionContext,next: CallHandler,
): Promise<Observable<any>> {const key = this.trackBy(context);const ttlValueOrFactory =this.reflector.get(CACHE_TTL_METADATA, context.getHandler()) ??this.reflector.get(CACHE_TTL_METADATA, context.getClass()) ??null;if (!key) {return next.handle();}try {const value = await this.cacheManager.get(key);if (!isNil(value)) {return of(value);}const ttl = isFunction(ttlValueOrFactory)? await ttlValueOrFactory(context): ttlValueOrFactory;return next.handle().pipe(tap(async response => {if (response instanceof StreamableFile) {return;}const args = [key, response];if (!isNil(ttl)) {args.push(this.cacheManagerIsv5OrGreater ? ttl : { ttl });}try {await this.cacheManager.set(...args);} catch (err) {Logger.error(`An error has occurred when inserting "key: ${key}", "value: ${response}"`,'CacheInterceptor',);}}),);} catch {return next.handle();}
}

CacheInterceptor的执行函数首先通过trackBy方法获取到缓存的key,如果key存在,调用CacheManager获取缓存,缓存存在,返回。如果没有缓存,追加了一个response的管道(pipe),做一个response的缓存。
这里有两个点:

  • 缓存的key是什么
  • 缓存的时间是多少

缓存的key

看下trackBy

protected trackBy(context: ExecutionContext): string | undefined {const httpAdapter = this.httpAdapterHost.httpAdapter;// 是否为Http请求const isHttpApp = httpAdapter && !!httpAdapter.getRequestMethod;// 获取上下文中CACHE_KEY_METADATA的信息const cacheMetadata = this.reflector.get(CACHE_KEY_METADATA,context.getHandler(),);if (!isHttpApp || cacheMetadata) {return cacheMetadata;}const request = context.getArgByIndex(0);if (!this.isRequestCacheable(context)) {return undefined;}return httpAdapter.getRequestUrl(request);}

如果是Http请求的话,会调用isRequestCacheable方法判断是否需要缓存。
isRequestCacheable方法:

protected allowedMethods = ['GET'];protected isRequestCacheable(context: ExecutionContext): boolean {const req = context.switchToHttp().getRequest();return this.allowedMethods.includes(req.method); // 判断是否是允许缓存的请求类型
}

这里只有GET请求会被缓存。缓存的key为trackBy返回的httpAdapter.getRequestUrl(request);即当前的请求的url。

缓存的有效时间

CacheInterceptor通过下面的方式来获取默认ttl(生存时间)的方法:

 const ttlValueOrFactory =this.reflector.get(CACHE_TTL_METADATA, context.getHandler()) ??this.reflector.get(CACHE_TTL_METADATA, context.getClass()) ??null;

就获取上下文中CACHE_TTL_METADATA的配置的方法。在设置缓存的时候使用这个方法:

if (!isNil(ttl)) {args.push(this.cacheManagerIsv5OrGreater ? ttl : { ttl });
}

假如没有设置这时间,那么就会使用默认的缓存有效时间。这个时间是5s。
在这里插入图片描述

怎么解决

ok,原因理清楚了,那么怎么解决这个问题呢?

1.修改url

最简单的做法,既然使用url来做key,那么只要让每次请求的url不一样就行了,前端在每次请求的时候都带上一个时间戳改变url。

`/todo/get-list?v=${+new Date()}`

但是,简单归简单,这方法实在太不优雅了。

2.重载CacheInterceptor

既然是因为引入了CacheInterceptor导致的问题,但是项目又需要缓存拦截器,那么自己写一个CacheInterceptor也是一个好办法:

import { CacheInterceptor } from '@nestjs/common';export class RequestCacheInterceptor extends CacheInterceptor {protected isRequestCacheable(): boolean {return false;}
}

这里我写了一个RequestCacheInterceptor继承了CacheInterceptor,然后重载了isRequestCacheable方法,直接返回false。其他的方法保持不变。然后引入这个Interceptor:

@Module({imports: [],controllers: [],providers: [{provide: APP_INTERCEPTOR,useClass: RequestCacheInterceptor},
});

这样就取消了GET请求的缓存。

方案扩展

上面通过重构CacheInterceptor取消了GET请求的缓存,那如果部分接口需要缓存,部分不需要缓存(按需)要怎么实现呢?

1.清单管理

我们可以通过一个清单把需要缓存(或者不缓存)的接口管理起来,在CacheInterceptor中判断是否需要缓存。

allowed-cache-api.ts

export default ['/todo/get-list'
];
import { CacheInterceptor } from '@nestjs/common';
import AllowedCacheApis from './allowed-cache-api';export class RequestCacheInterceptor extends CacheInterceptor {protected isRequestCacheable(context: ExecutionContext): boolean {// 获取当前请求的信息const req = context.switchToHttp().getRequest();// 如果是允许缓存的接口if (AllowedCacheApis.includes(req.path)) {return true;}return false;}
}

这个方法可以有效的区分需要缓存和不缓存的接口,但是需要再全局维护一个缓存清单,如果接口数量比较大或者是默认缓存需要维护不缓存的接口,那就很容易出现问题。有没有其他办法呢?

2.在controller中维护清单

假如我们把全局清单去掉,在每个controller中去维护这个清单,是否也可以?
在controller中加入清单:

@Controller('todo')
export class TodoController {constructor(private readonly service: Service) {}// 这里需要使用静态变量,否则在上下文内不好读取static allowedCacheApis = ['getList'];@AllowedCache()@Get('get-list')getList(): string {.....}
}

拦截器中:

import { CacheInterceptor } from '@nestjs/common';export class RequestCacheInterceptor extends CacheInterceptor {protected isRequestCacheable(context: ExecutionContext): boolean {// 获取当前类const currentClass = context.getClass();// 获取当前方法const currHandler = context.getHandler().name;// 如果是允许缓存的接口if (currClass.allowedCacheApis.includes(currHandler)) {return true;}return false;}
}

在拦截器中,通过运行上下文,获取到当前执行的controller,并取出清单。然后再获取当前执行的接口方法名进行判断。这里需要注意的是,获取到的是接口方法名getList而不是接口路径get-list,因为这里context.getHandler()获取的是方法Function getList,其namegetList

虽然这么处理可以把清单分散到各个入口中自行管理,但是感觉还是不够优雅,有没有更优雅的解决方案呢?

3.装饰器

更优雅的解决方案,最好当然是在定义接口的时候就把要不要缓存也描述了,按照nestjs的风格,给接口加一个描述的装饰器来描述接口缓存与否应该是最符合nestjs的风格的方式,也是比较优雅的方式吧。
首先,先自定义一个装饰器:
allowed-cache-decorator.ts

import { SetMetadata, applyDecorators } from '@nestjs/common';// 允许缓存的装饰器
export function AllowedCache() {// 添加一个metadata信息allowedCache为true,表示允许缓存return applyDecorators(SetMetadata('allowedCache', true));
}

在接口中添加装饰器

import { AllowedCache } from './allowed-cache-decorator';@Controller('todo')
export class TodoController {constructor(private readonly appService: AppService) {}// get-list接口@AllowedCache()@Get('get-list')getList(@Query() { name }): string {console.log('controller.....');console.log(name);return this.appService.getHello();}
}

拦截器中:

import {CacheInterceptor,ExecutionContext
} from '@nestjs/common';export class RequestCacheInterceptor extends CacheInterceptor {protected isRequestCacheable(context: ExecutionContext): boolean {// CacheInterceptor默认引入Reflector,可以通过Reflector获取上下文中写入meta dataconst allowed = this.reflector.get('allowedCache', context.getHandler());return !!allowed;}
}

总结

本文是由于一个非正常现象引起发的一系列的探索和思考的记录,如果有缺漏或错误,请不吝指出。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com