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 =https://www.cnblogs.com/lyps/p/ 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 = https://www.cnblogs.com/lyps/p/JsonSerializer.Serialize((filterContext.Result as Microsoft.AspNetCore.Mvc.ObjectResult).Value); //如果快取null,則設定較短過期時間(此處是防止快取穿透) var disData = https://www.cnblogs.com/lyps/p/JsonSerializer.Deserializestring, 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別人的文章,還是很敬佩哪些經常更新博客的大佬,首先文章要有技術點、然后還要考慮怎樣將自己對技術點的想法、經驗、理解表達出來,真的很不容易,然后就是文章有什么錯誤點或者可以改進的地方望指正,
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/66454.html
標籤:.NET Core
上一篇:Fireasy 官網改版
