.NetCore之接口缓存

矮人狙击手
• 阅读 136

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

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

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

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

.NetCore之接口缓存; "复制代码")

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);

    } 

.NetCore之接口缓存; "复制代码")

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

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

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

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

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

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

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

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

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

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

6、最后,已经很久没更新博客了,是我太懒了,只想白p别人的文章。还是很敬佩哪些经常更新博客的大佬,首先文章要有技术点、然后还要考虑怎样将自己对技术点的想法、经验、理解表达出来,真的很不容易。然后就是文章有什么错误点或者可以改进的地方望指正。

点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
3年前
Spring Cloud Eureka解析(3) EurekaClient 重要缓存解析
EurekaClient也存在缓存,应用服务实例列表信息在每个EurekaClient服务消费端都有缓存。一般的,Ribbon的LoadBalancer会读取这个缓存,来知道当前有哪些实例可以调用,从而进行负载均衡。这个loadbalancer同样也有缓存。首先看这个LoadBalancer的缓存更新机制,相关类是PollingServerListUpd
缓存空间优化实践
缓存Redis,是我们最常用的服务,其适用场景广泛,被大量应用到各业务场景中。也正因如此,缓存成为了重要的硬件成本来源,我们有必要从空间上做一些优化,降低成本的同时也会提高性能。下面以我们的案例说明,将缓存空间减少70%的做法。
Easter79 Easter79
3年前
Spring实例化bean之后的处理, 关于BeanPostProcessor接口的使用
业务需求:缓存页面,展示需要缓存的所有对象,每类对象在字典表中有编码对应,点击某个对象可以缓存某类对象,每类对象都有自己的缓存runner(弱弱的说一句,本人看到这里的第一反应就是ifelse,捂脸中。。。。。。。。。。。)方法:经经理指导,使用BeanPostProcessor接口逻辑:自定义一个标签,spring实例化所有bean之后,取出每个
Stella981 Stella981
3年前
Guava的两种本地缓存策略
Guava的两种缓存策略缓存在很多场景下都需要使用,如果电商网站的商品类别的查询,订单查询,用户基本信息的查询等等,针对这种读多写少的业务,都可以考虑使用到缓存。在一般的缓存系统中,除了分布式缓存,还会有多级缓存,在提升一定性能的前提下,可以在一定程度上避免缓存击穿或缓存雪崩,也能降低分布式缓存的负载。Guav
Stella981 Stella981
3年前
Redis 缓存穿透、缓存雪崩的概念及其预防
缓存穿透【什么是缓存穿透】频繁查询不在缓存中的数据,给原本被缓存保护的系统过大压力。【为什么会发生缓存穿透】1\.程序没写好;2\.恶意攻击。【怎样防止缓存穿透】1\.在对key进行查询之前,先做初步判断,如果key一定不存在(例如,对某表的缓存,key一定由数字组成,那么包含非数字的key一定是不存在的
Stella981 Stella981
3年前
Redis之缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级
\TOC\Redis之缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级1、缓存雪崩  发生场景:当Redis服务器重启或者大量缓存在同一时期失效时,此时大量的流量会全部冲击到数据库上面,数据库有可能会因为承受不住而宕机  解决办法:    1)随机均匀设置失效
Stella981 Stella981
3年前
Redis缓存被污染了,该怎么办?
在一些场景下,有些数据被访问的次数非常少,甚至只会被访问一次。当这些数据服务完访问请求后,如果还继续留存在缓存中的话,就只会白白占用缓存空间。这种情况,就是缓存污染。1.如何解决缓存污染问题?要解决缓存污染,我们也能很容易想到解决方案,那就是得把不会再被访问的数据筛选出来并淘汰掉。这样就不用等到缓存被写满以后,再逐一淘汰旧
Stella981 Stella981
3年前
Linux玩转redis从入门到放肆
1\.缓存穿透在大多数互联网应用中,缓存的使用方式如下图所示:!(https://oscimg.oschina.net/oscnet/6a12e0fbee579fa624b2ea1738e89278c3f.png)1.当业务系统发起某一个查询请求时,首先判断缓存中是否有该数据;2.如果缓存中存在,则直接返回数据;3.如果缓存中
Stella981 Stella981
3年前
Redis缓存的基本思想
1.缓存的基本思想很多朋友,只知道缓存可以提高系统性能以及减少请求相应时间,但是,不太清楚缓存的本质思想是什么。缓存的基本思想其实很简单,就是我们非常熟悉的空间换时间。不要把缓存想的太高大上,虽然,它的确对系统的性能提升的性价比非常高。其实,我们在学习使用缓存的时候,你会发现缓存的思想实际在操作系统或者其他地方都被大量用到。比如C
Wesley13 Wesley13
3年前
JS浏览器不缓存页面的几种方法
我们需不需要浏览器缓存?浏览器缓存,有时我们需要,有时我们不需要,就比如股票类型的网页就需要实时刷新数据,不能让页面从缓存里读取数据,如果对于一些不需要实时更新数据的网站来说,浏览器缓存可以提高加载速度,带来更好的用户体验,到底需不需要浏览器缓存,让我们自己操作!meta方法//不缓存<METAHTTPEQU
由 Mybatis 源码畅谈软件设计(八):从根上理解 Mybatis 二级缓存
上一篇我们介绍了一级缓存。本篇则是关于二级缓存,同样地,仍然需要关注它的实现原理,以及要考虑“为什么在已经有了一级缓存的情况下还需要设计二级缓存”的问题,在以后实际业务中的缓存设计提供借鉴和参考。1.验证二级缓存在上一篇帖子中的User和Departmen