在开发的时候我们经常会遇到API请求慢导致页面加载速度慢,以及每次刷新的时候都要去API请求数据(大部分可能没有新数据,从而造成重复的且无效的请求)。
当我们想全站静态化的时候,却因为量太大,以及数据更新不及时会导致的一系列头疼的问题。
且我们在使用OSS和k8s,如果静态化,那务必又需要把大量的html文件存入oss。
那么新增另外一种缓存解决方案,或许能解决一部分的问题,分别为两种方式:
- 请求的时候缓存数据(设定有效期)
- 请求后,同时后置被动刷新缓存(用户永远都请求的缓存,不需要造成多余且浪费的请求API服务资源)
实现原理图
Web项目如何使用?
1、引用包
<PackageReference Include="XUCore.NetCore.AspectCore" Version="1.0.9" />
2、web项目 Program.cs文件
我们在CreateDefaultBuilder后面注入服务
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
//......其他代码省略
//注入缓存任务,这里的作用是让其AOP起作用
.UseCacheTaskHostBuilder();
3、在ConfigureServices中注册服务
/// <summary>
/// 注册扫描服务类
/// </summary>
public void AddScan()
{
services.Scan(scan =>
scan.FromAssemblyOf<IService>()
.AddClasses(impl => impl.AssignableTo(typeof(IService)))
.AsImplementedInterfaces()
.WithSingletonLifetime() //需要注意的是,如果使用后置缓存更新,所有被缓存的服务都需要设置为单例
);
services.Scan(scan =>
scan.FromAssemblyOf<IHttpSignPolicy>()
.AddClasses(impl => impl.AssignableTo(typeof(IHttpSignPolicy)))
.AsImplementedInterfaces()
.WithSingletonLifetime() //需要注意的是,如果使用后置缓存更新,所有被缓存的服务都需要设置为单例
);
//注册缓存服务,暂时只提供内存缓存,后面会增加Redis,需要视情况而定
services.AddCacheTaskService<MemoryCacheService>();
}
4、在Configure中启用服务
/// <summary>
/// 启用静态请求上下文,无需从构造函数引入(Web.IP、Web.Service<T>())
/// </summary>
public void UseStaticHttpContext()
{
//启用静态请求上下文(必须开启)
app.UseStaticHttpContext();
//启用真实IP中间件
app.UseRealIp();
//*******启用缓存服务*******
app.UseCacheTaskService(lifetime);
app.UseCookiePolicy();
app.UseResponseCaching();
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = context =>
{
context.Context.Response.GetTypedHeaders().CacheControl = new Microsoft.Net.Http.Headers.CacheControlHeaderValue
{
Public = true,
//for 7 days
MaxAge = System.TimeSpan.FromDays(7)
};
}
});
}
到这里注册完毕,接下来如何使用?
web项目中会有很多Http请求的服务类,那么我们只需要使用标签CacheInterceptor进行设置即可实现缓存。
/// <summary>
/// 根据广告位获取广告
/// </summary>
/// <param name="ids"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[CacheInterceptor(Key = "AdsService_GetAdsByPositionIdsList", ParamterKey = "{0}", CacheSeconds = CacheTime.Min5)]
public async Task<List<AdsAdsInationOutPutModel>> GetAdsByPositionIdsList(int[] ids, CancellationToken cancellationToken)
{
var url = UrlArguments.Create(ApiClient.AdsService, $"api/AdsAdsApi/GetAdsByPositionIdsList");
var res = await httpMessageService.PostAsync(url, ids, cancellationToken: cancellationToken, errorHandler: ApiRequestException.ErrorHandler<List<AdsAdsInationOutPutModel>>);
return res.bodyMessage ?? new List<AdsAdsInationOutPutModel>();
}
那么两种方式,怎么使用?
一、请求被缓存(主动方式)
其实这种方式是最常见的,也就是请求后得到数据,将此数据缓存起来。(常见代码就不帖了)
[CacheInterceptor(Key = "AdsService_GetAdsByPositionIdsList", ParamterKey = "{0}", CacheSeconds = CacheTime.Min5)]
Key 缓存key ParamterKey 参数key,{0}为string.format语法,对应方法的参数位置
这种方式就是请求后,通过AOP拿到返回的数据,通过设置的key和参数结合为CacheKey以及缓存时间,保存到内存,然后到了过期了就继续执行请求拿数据。
简单例子如下:
var entry = cacheService.Get(key);
if (entry == null)
{
await next(context);
if (context.ReturnValue != null)
cacheService.Set(key, TimeSpan.FromSeconds(CacheSeconds), context.ReturnValue);
}
else
context.ReturnValue = entry;
完整使用示例
在客户访问一个交易商后,被缓存起来,在1个小时内任何客户访问该交易商都是缓存数据。也可以说1个小时内这条数据只请求一次API。
只是我通过AOP简化了缓存的步骤,通过配置的方式缓存。
注意:
该方式没有找到缓存的时候会请求方法拿数据。
适用于缓存文章详情、交易商详情等。因为我们并不知道用户会看哪个数据,所以不适合把所有详情数据都自动同步到web里。
二、请求后置刷新缓存(被动方式)
这种方式第一次请求拿数据和第一种方式一致,但是在拿到数据后会永久性缓存(只要服务不重启),此时会建立后台调度任务进行调度,任务的执行周期则是设置的CacheSeconds参数。
提醒:
由于后置刷新缓存需要调度方法,所以在使用该方式的时候必须要注意,服务一定得是单例模式,不然请求后服务会被释放掉,任务无法访问一个已经被释放的资源。
使用方式如下:
[CacheInterceptor(IsTigger = true, Key = "ReputationService_GetMonthRank", ParamterKey = "{0}_{1}_{2}_{3}", CacheSeconds = CacheTime.Min2)]
IsTigger 是否开启后台触发同步缓存,该开关必须打开才会开启后置同步,Key 缓存key ParamterKey 参数key,{0}为string.format语法,对应方法的参数位置 ,CacheSeconds 则是缓存同步周期。
完整使用示例
该示例演示的是需要在2分钟的频率更新汇查查口碑排行。或许比如我们在接入行情数据的时候可以5秒一次?无所谓,因为一个web只5秒请求一次API,扛得住,不比每个客户端5秒请求一次。
这样能有效阻断并发导致API服务器压力增大。
注意:
该方式适用于一次性拿数据的请求,不适合根据参数拿数据的请求
比如:
交易商分页?文章分页?不适合,分页类的都不适合
获取相对实时的数据?适合,为了避免前端的js疯狂请求拿取数据导致API服务器扛不住?那么这个时候我们可以通过这种方式阻断前端请求。被动更新数据。
具体实现源码可在 XUCore.NetCore.AspectCore 查看
在k8s中运行情况
最后特别提醒:
不管是哪一种方式,我们需要斟酌具体的使用场景来选择哪个方式合适。过度使用都会出现不可预测的问题。
如果是整站使用,则会导致内存不足的情况,所以选择性的优化使用。如果整站使用还不如全站静态化。
~