当前位置 博文首页 > .NetCore之接口缓存的实现示例

    .NetCore之接口缓存的实现示例

    作者:一只小菜鸟都被占了 时间:2021-09-08 17:43

    1、问题:我们平时做开发的时候肯定都有用到缓存这个功能,一般写法是在需要的业务代码里读取缓存、判断是否存在、不存在则读取数据库再设置缓存这样一个步骤。但是如果我们有很多地方业务都有用到缓存,我们就需要在每个地方都写关于缓存的代码,这样会造成很多重复代码,同时对业务侵入不利于后续的开发维护。

    2、一般的解决办法是将缓存的功能提取出来,然后在需要用到缓存的地方调用即可。这样确实减少了很多重复代码,但这样还是会存在整个项目通用的缓存功能侵入业务代码,那我们有什么办法将缓存功能完全提取出来,达到业务代码零侵入呢?

    3、既然我们缓存存的是接口的业务数据,那么为何我们不能直接把整个接口缓存起来呢,即将整个接口返回的数据缓存?同时要达到业务零侵入,那我们是不是想到了反射、特性呢?没错,我们使用的就是ActionFilterAttribute,关于ActionFilterAttribute无非就是OnActionExecuting(执行动作方法前触发)、OnActionExecuted(执行动作方法后触发)、OnResultExecuting(在执行操作结果之前触发)、OnResultExecuted(在执行操作结果之后触发)这四个方法,相信很多小伙伴都用到过,这里就不细说了。那我们现在的解决方案是:在OnActionExecuting(执行动作方法前触发)里判断是否存在缓存,如果存在则不去执行接口业务,直接返回数据。还有一个问题,一般接口都会有入参,入参不同输出的数据也不同(比如我有一个分页的接口,传的page参数不同,得到的结果也不同),这个怎么解决呢?我们只需要把接口所有参数拼凑起来,然后MD5加密成一个字符串,将其作为缓存的key,那么即使同一个接口、参数不同也会得到不同的key。

    4、废话不多说,直接上代码。

    public class ApiCache : ActionFilterAttribute
     {
      /// <summary>
      /// Header是否参与缓存验证
      /// </summary>
      public bool SignHeader = false;
      /// <summary>
      /// 缓存有效时间(分钟)
      /// </summary>
      public int CacheMinutes = 5;/// <summary>
      /// 
      /// </summary>
      /// <param name="SignHeader">Header是否参与请求体签名</param>
      /// <param name="CacheMinutes">缓存有效时间(分钟)</param>
      public ApiCache(bool SignHeader = false, int CacheMinutes = 5)
      {
       this.SignHeader = SignHeader;
       this.CacheMinutes = CacheMinutes;
      }
    
    
      public override void OnActionExecuting(ActionExecutingContext filterContext)
      {
       //请求体签名
       string cacheKey = getKey(filterContext.HttpContext.Request);
       //根据签名查询缓存
       string data = CsRedisHepler.Get(cacheKey);
       if (!string.IsNullOrWhiteSpace(data))
       {
        //有缓存则设置返回信息
        var content = new Microsoft.AspNetCore.Mvc.ContentResult();
        content.Content = data;
        content.ContentType = "application/json; charset=utf-8";
        content.StatusCode = 200;
        filterContext.HttpContext.Response.Headers.Add("ContentType", "application/json; charset=utf-8");
        filterContext.HttpContext.Response.Headers.Add("CacheData", "Redis");
        filterContext.Result = content;
       }
      }
    
      public override void OnActionExecuted(ActionExecutedContext filterContext)
      {
       base.OnActionExecuted(filterContext);
      }
    
      public override void OnResultExecuting(ResultExecutingContext filterContext)
      {
       base.OnResultExecuting(filterContext);
      }
    
      public override void OnResultExecuted(ResultExecutedContext filterContext)
      {
       if (filterContext.HttpContext.Response.Headers.ContainsKey("CacheData")) return;
       //获取缓存key
       string cacheKey = getKey(filterContext.HttpContext.Request);
       var data = JsonSerializer.Serialize((filterContext.Result as Microsoft.AspNetCore.Mvc.ObjectResult).Value);
       //如果缓存null,则设置较短过期时间(此处是防止缓存穿透)
       var disData = JsonSerializer.Deserialize<Dictionary<string, object>>(data);
       if(disData.ContainsKey("data") && disData["data"]==null)
       {
        CacheMinutes = 1;
       }
       CsRedisHepler.Set(cacheKey, data, TimeSpan.FromMinutes(CacheMinutes));
      }
      /// <summary>
      /// 请求体MDH签名
      /// </summary>
      /// <param name="request"></param>
      /// <returns></returns>
      private string getKey(HttpRequest request)
      {
       var keyContent = request.Host.Value + request.Path.Value + request.QueryString.Value + request.Method + request.ContentType + request.ContentLength;
       try
       {
        if (request.Method.ToUpper() != "DELETE" && request.Method.ToUpper() != "GET" && request.Form.Count > 0)
        {
         foreach (var item in request.Form)
         {
          keyContent += $"{item.Key}={item.Value.ToString()}";
         }
        }
       }
       catch (Exception e)
       {
    
       }
       if (SignHeader)
       {
        var hs = request.Headers.Where(a => !(new string[] { "Postman-Token", "User-Agent" }).Contains(a.Key)).ToDictionary(a => a);
        foreach (var item in hs)
        {
         keyContent += $"{item.Key}={item.Value.ToString()}";
        }
       }
           //md5加密
       return CryptographyHelper.MD5Hash(keyContent);
      }   

    这里使用的是redis,也可以选择其他的,代码简单没有做适配,这样我们只需要在用到缓存的接口上加上[ApiCache(CacheMinutes =1)]特性就行啦,关于参数的话也可以根据自己的业务需求来定制。

    5、关于缓存的三座大山:缓存穿透、缓存击穿、缓存雪崩,这块网上有很多的资料可以看,这里只做一个简单的介绍跟解决思路。

    缓存穿透:访问一个不存在的key时,请求会穿过缓存直接请求数据库。比如现在有个接口是分页的,然后客户端请求接口的时候将pageindex参数给的很大,大到该接口不可能有这么多页的数据时,每次请求都会穿过缓存去查数据库。如果有人故意攻击接口就会给数据库造成巨大压力甚至挂掉。当然,这里我们肯定也要做一些业务参数的校验,比如每页条数不能超过多少之类的,总之不能轻信客户端传过来的参数。

    解决方案:最简单有效的解决方案是当在数据库也查不到数据的时候,设置一个value为null的缓存值(该值的过期时间要尽量短),这样就可以避免恶意攻击。另外就是使用布隆过滤器。

    我们这里使用的解决方案是第一种设置null值,在上述的代码中有注释。不过这里最好接口有一个返回规范,比如每个接口返回固定值:message、code、data这几个字段, 那么我们只需判断data是否为空来设置过期时间。

    缓存击穿:某一个访问量极高的key过期,导致所有请求打在数据库上。

    解决方案:将访问量高德key设置永不过期、使用互斥锁。我们这里使用设置key永不过期就行,具体实现就是加一个是否过期的字段从外部传入,再根据该字段判断是否设置过期时间。同时可以写一个定时任务去更新设置为永不过期的key值。

    缓存雪崩:某一时刻多个高访问量的key同时过期。

    解决方案:在设置过期时间的时候将每个key的过期时间设置分布开来,在上述代码中CacheMinutes字段改成过期时间范围从。。。到。。。,然后key的过期时间从范围中取一个随机值。

    当然这里讲到的解决方案也只是个人常用的,也可以使用其他解决方案。

    jsjbwy