.NetCore之接口緩存


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的過期時間從范圍中取一個隨機值。

當然這里講到的解決方案也只是個人常用的,也可以使用其他解決方案。

6、最后,已經很久沒更新博客了,是我太懶了,只想白p別人的文章。還是很敬佩哪些經常更新博客的大佬,首先文章要有技術點、然后還要考慮怎樣將自己對技術點的想法、經驗、理解表達出來,真的很不容易。然后就是文章有什么錯誤點或者可以改進的地方望指正。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM