程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> 那些年我們一起追過的緩存寫法(一),一起追過緩存寫法

那些年我們一起追過的緩存寫法(一),一起追過緩存寫法

編輯:C#入門知識

那些年我們一起追過的緩存寫法(一),一起追過緩存寫法


 

介紹

本篇主要說下樓主平常項目中緩存使用經驗和遇到過的問題。

 

目錄

一: 基本寫法

二:緩存雪崩

  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 = "395"; //這裡一般是 sql查詢數據。 例:395 簽到天數 // CacheHelper.Add(cacheKey, cacheValue, cacheTime); //} lock (this) { 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 = "395"; //這裡一般是 sql查詢數據。 例:395 簽到天數
            //    CacheHelper.Add(cacheKey, cacheValue, cacheTime);
            //}
            lock (lockKey)
            {
                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 = 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倍的時間,仍然可以能用髒數據給前端展現。   

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

 

五:總結

補充下: 這裡說的阻塞其他函數指的是,高並發下鎖同一對象。

實際使用中,緩存層封裝往往要復雜的多。  關於更新緩存,可以單開一個線程去專門跑這些,圖方便就扔線程池吧。

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

 

如有錯誤之處,歡迎指出糾正。  對您有幫助的,請推薦下n(*≧▽≦*)n。

作者:蘑菇先生      

出處:http://www.cnblogs.com/mushroom/p/4199701.html

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved