Web页面加速:Cache Http请求服务缓存计划

分类: NetCore

在开发的时候我们经常会遇到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中运行情况

最后特别提醒:

不管是哪一种方式,我们需要斟酌具体的使用场景来选择哪个方式合适。过度使用都会出现不可预测的问题。

如果是整站使用,则会导致内存不足的情况,所以选择性的优化使用。如果整站使用还不如全站静态化。

~

标签: NetCore

上一篇: 【NetCore】API【网络加速】输出自定义字段、重命名以及日期格式自定义的玩法。

下一篇: EasyQuartz的使用,让后台任务合理并有序的执行

by 2023-08-07 23:49:07
篇笔记

学习笔记