那些年我們一起追過的快取寫法(一)

蘑菇先生發表於2015-03-04

介紹

本篇主要說下樓主平常專案中快取使用經驗和遇到過的問題。

目錄

一: 基本寫法

二:快取雪崩

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倍的時間,仍然可以能用髒資料給前端展現。

這樣就能提高不少系統吞吐量了。

五:總結

補充下: 這裡說的阻塞其他函式指的是,高併發下鎖同一物件。

實際使用中,快取層封裝往往要複雜的多。 關於更新快取,可以單開一個執行緒去專門跑這些,圖方便就扔執行緒池吧。

具體使用場景,可根據實際使用者量來平衡。

那些年我們一起追過的快取寫法(二)

那些年我們一起追過的快取寫法(三)

相關文章