那些年我們一起追過的快取寫法(一)
介紹
本篇主要說下樓主平常專案中快取使用經驗和遇到過的問題。
目錄
一: 基本寫法
二:快取雪崩
1:全域性鎖,例項鎖
2:字串鎖
三:快取穿透
四:再談快取雪崩
五:總結
一:基本寫法
為了方便演示,我們用Runtime.Cache做快取容器,並定義個簡單操作類。如下:
public class CacheHelper { public static object Get(string cacheKey) { return HttpRuntime.Cache[cacheKey]; } public static void Add(string cacheKey, object obj, int cacheMinute) { HttpRuntime.Cache.Insert(cacheKey, obj, null, DateTime.Now.AddMinutes(cacheMinute), Cache.NoSlidingExpiration, CacheItemPriority.Normal, null); } }
簡單讀取:
public object GetMemberSigninDays1() { const int cacheTime = 5; const string cacheKey = "mushroomsir"; var cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) return cacheValue; cacheValue = "395"; //這裡一般是 sql查詢資料。 例:395 簽到天數 CacheHelper.Add(cacheKey, cacheValue, cacheTime); return cacheValue; }
在專案中,有不少這樣寫法。這樣寫沒有錯,但在併發量上來後就會有問題。繼續看
二:快取雪崩
快取雪崩是由於快取失效(過期),新快取未到期間。
這個中間時間內,所有請求都去查詢資料庫,而對資料庫CPU和記憶體造成巨大壓力,前端連線數不夠、查詢阻塞。
這個中間時間並沒有那麼短,比如sql查詢1秒,加上傳輸解析0.5秒。 就是說1.5秒內所有使用者查詢,都是直接查詢資料庫的。
這種情況下,我們想到最多的就是加鎖排隊了。
1:全域性鎖,例項鎖
public static object obj1 = new object(); public object GetMemberSigninDays2() { const int cacheTime = 5; const string cacheKey = "mushroomsir"; var cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) return cacheValue; //lock (obj1) //全域性鎖 //{ // cacheValue = CacheHelper.Get(cacheKey); // if (cacheValue != null) // return cacheValue; // cacheValue = "395"; //這裡一般是 sql查詢資料。 例:395 簽到天數 // CacheHelper.Add(cacheKey, cacheValue, cacheTime); //} lock (this) { cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) return cacheValue; cacheValue = "395"; //這裡一般是 sql查詢資料。 例:395 簽到天數 CacheHelper.Add(cacheKey, cacheValue, cacheTime); } return cacheValue; }
第一種:lock (obj1) 是全域性鎖可以滿足,但我們要為每個函式都宣告一個obj,不然在A、B函式都鎖obj1時,必然會讓其中一個阻塞。
第二種:lock (this) 這個鎖當前例項,對其他例項無效,這個鎖就沒什麼效果了。使用單例模式的可以鎖。
但在當前例項中:A函式鎖當前例項,其他鎖當前例項的函式讀寫,也被阻塞。 不可取
2:字串鎖
既然鎖物件不行,利用字串的特性,我們直接鎖快取key呢。來看下
public object GetMemberSigninDays3() { const int cacheTime = 5; const string cacheKey = "mushroomsir"; var cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) return cacheValue; const string lockKey = cacheKey + "n(*≧▽≦*)n"; //lock (cacheKey) //{ // cacheValue = CacheHelper.Get(cacheKey); // if (cacheValue != null) // return cacheValue; // cacheValue = "395"; //這裡一般是 sql查詢資料。 例:395 簽到天數 // CacheHelper.Add(cacheKey, cacheValue, cacheTime); //} lock (lockKey) { cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) return cacheValue; cacheValue = "395"; //這裡一般是 sql查詢資料。 例:395 簽到天數 CacheHelper.Add(cacheKey, cacheValue, cacheTime); } return cacheValue; }
第一種:lock (cacheName) 有問題,因為字串也是共享的,會阻塞其他使用這個字串的操作行為。 具體請看之前的博文 c#語言-多執行緒中的鎖系統(一)。
2015-01-04 13:36更新:因為字串被公共語言執行庫 (CLR)暫留,這意味著整個程式中任何給定字串都只有一個例項。所以才會用第二種
第二種:lock (lockKey) 可以滿足。其實目就是為了保證鎖的粒度最小並且全域性唯一性,只鎖當前快取的查詢行為。
三:快取穿透
舉個簡單例子:一般我們會快取使用者搜尋結果。而資料庫查詢不到,是不會做快取的。但如果頻繁查這個關鍵字,就會每次都直查資料庫了。
這樣快取就沒意義了,這也是常提的快取命中率問題。
public object GetMemberSigninDays4() { const int cacheTime = 5; const string cacheKey = "mushroomsir"; var cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) return cacheValue; const string lockKey = cacheKey + "n(*≧▽≦*)n"; lock (lockKey) { cacheValue = CacheHelper.Get(cacheKey); if (cacheValue != null) return cacheValue; cacheValue = null; //資料庫查詢不到,為空。 //if (cacheValue2 == null) //{ // return null; //一般為空,不做快取 //} if (cacheValue == null) { cacheValue = string.Empty; //如果發現為空,我設定個預設值,也快取起來。 } CacheHelper.Add(cacheKey, cacheValue, cacheTime); } return cacheValue; }
例子中我們把查詢不到的結果,也給快取起來了。這樣就可以避免,查詢為空時,引起快取穿透了。
當然我們也可以單獨設定個快取區,進行第一層控制校驗。 以便和正常快取區分開了。
四:再談快取雪崩
額 不是用加鎖排隊方式就解決了嗎?其實加鎖排隊只是為了減輕DB壓力,並沒有提高系統吞吐量。
在高併發下: 快取重建期間,你是鎖著的,1000個請求999個都在阻塞的。 使用者體驗不好,還浪費資源:阻塞的執行緒本可以處理後續請求的。
public object GetMemberSigninDays5() { const int cacheTime = 5; const string cacheKey = "mushroomsir"; //快取標記。 const string cacheSign = cacheKey + "_Sign"; var sign = CacheHelper.Get(cacheSign); //獲取快取值 var cacheValue = CacheHelper.Get(cacheKey); if (sign != null) return cacheValue; //未過期,直接返回。 lock (cacheSign) { sign = CacheHelper.Get(cacheSign); if (sign != null) return cacheValue; CacheHelper.Add(cacheSign, "1", cacheTime); ThreadPool.QueueUserWorkItem((arg) => { cacheValue = "395"; //這裡一般是 sql查詢資料。 例:395 簽到天數 CacheHelper.Add(cacheKey, cacheValue, cacheTime*2); //日期設快取時間的2倍,用於髒讀。 }); } return cacheValue; }
程式碼中,我們多用個快取標記key,雙檢鎖校驗。它設定為正常時間,過期後通知另外的執行緒去更新快取資料。
而實際的快取由於設定了2倍的時間,仍然可以能用髒資料給前端展現。
這樣就能提高不少系統吞吐量了。
五:總結
補充下: 這裡說的阻塞其他函式指的是,高併發下鎖同一物件。
實際使用中,快取層封裝往往要複雜的多。 關於更新快取,可以單開一個執行緒去專門跑這些,圖方便就扔執行緒池吧。
具體使用場景,可根據實際使用者量來平衡。
相關文章
- 那些年我們一起擼過的快取寫法快取
- 那些年,我們一起追過的APPAPP
- 那些年,我們一起追過的化學元素
- 那些年,我們一起追尋的非同步程式設計非同步程式設計
- 那些年我們一起踩過的Dubbo坑
- 那些年,我們一起誤解過的RESTREST
- 那些年,我們一起走過的iOS推送的坑iOS
- 那些年 我追過的語言
- SQL Server DBA:那些年,我們一起用過的工具FASQLServer
- 那些年,我們一起做過的 Java 課後練習題(71 - 75)Java
- 那些年,我們一起做過的 Java 課後練習題(1 - 5)Java
- 純技術團隊創業,那些年我們一起走過的彎路創業
- 四萬字歌詞分析:那些年,我們一起追的五月天到底在唱什麼?
- 《我們一起進大廠》系列-快取雪崩、擊穿、穿透快取穿透
- 那些年,我們解析過的前端異常前端
- [圖靈贈書]那些年,我們追過的程式語言(第三期)圖靈
- IT人,那些年,一起踩過的坑
- 那些年,我們處理過的SQL問題SQL
- 我們們一起來談談,redis為什麼快?Redis
- 淺談快取寫法(一):快取的雪崩和穿透快取穿透
- 那些年,我們用過的伺服器軟體伺服器
- 致畢業生:那些年我們錯過的“BAT”BAT
- 聊聊我們那些年用過的表示式引擎元件元件
- 那些年,我們見過的 Java 服務端“問題”Java服務端
- 【高德地圖API】那些年我們一起開發的APP—即LBS應用模式分享地圖APIAPP模式
- 回溯演算法 | 追憶那些年曾難倒我們的八皇后問題演算法
- 我們有線上社群啦!快來加入一起玩兒~
- 那些年我玩過的程式語言(一)
- Python:那些年我們遇到的坑Python
- 我們們一起聊聊Java異常Java
- 2019年我們追過的jQuery,它的漏洞你知道嗎?jQuery
- 那些年我們賺過的外快(POS(移動支付)介面開發)
- 那些年 IT大佬們吹過的牛逼
- 小程式開發,那些我們跳過的坑
- 那些年一起用過的iOS開發利器之ParseiOS
- 那些年我看過的前端書前端
- 十年前那些我們曾迷戀過的網頁網頁
- 高速輸出-我們戲說快取快取