無縫的緩存讀取:雙存儲緩存策略

 

最近在作一個WEB的數據統計的優化,可是因爲數據量大,執行一次SQL統計要比較長的時間(通常700ms算是正常)。數據庫

正常的作法只要加個緩存就行了。緩存

可是同時業務要求此數據最多1分鐘就要更新,並且這一分種內數據可能會有較多變化(並且原系統不太易擴展)。數據結構

也就是說緩存1分鐘就要失效從新統計,並且用戶訪問這頁還非常頻繁,若是使用通常緩存那麼用戶體驗不好並且很容易形成超時。函數

 

看到以上需求,第一個進入我大腦的就是從前作遊戲時接觸到的DDraw的雙緩衝顯示方式。性能

image

在第一幀顯示的同時,正在計算第二幀,這樣讀取和計算就能夠分開了,也就避免了讀取時計算,提升了用戶體驗。測試

我想固然咱們也能夠將這種方式用於緩存的策略中,但這樣用空間換取時間的方式仍是得權衡的,由於並非全部時候都值得這麼作,但這裏我以爲這樣作應該是最好的方式了。優化

注:爲了能夠好好演示,本篇中的緩存都以IEnumerable的形式來存儲,固然這個文中原理也能夠應用在WebCache中。this

這裏我使用如下數據結構作爲存儲單元:spa

namespace CHCache {
    /// <summary>     /// 緩存介質     /// </summary>     public class Medium {
        /// <summary>         /// 主要存儲介質         /// </summary>         public object Primary { get; set; }
        /// <summary>         /// 次要存儲介質         /// </summary>         public object Secondary { get; set; }
        /// <summary>         /// 是否正在使用主要存儲         /// </summary>         public bool IsPrimary { get; set; }
        /// <summary>         /// 是否正在更新         /// </summary>         public bool IsUpdating { get; set; }
        /// <summary>         /// 是否更新完成         /// </summary>         public bool IsUpdated { get; set; }
    }
}

有了這個數據結構咱們就能夠將數據實現兩份存儲。再利用一些讀寫策略就能夠實現上面咱們講的緩存方式。線程

整個的緩存咱們使用以下緩存類來控制:

/* * http://www.cnblogs.com/chsword/ * chsword * Date: 2009-3-31 * Time: 17:00 * */ using System;using System.Collections;using System.Collections.Generic;using System.Threading;namespace CHCache {
    /// <summary>     /// 雙存儲的類     /// </summary>     public class DictionaryCache : IEnumerable {
        /// <summary>         /// 在此緩存構造時初始化字典對象         /// </summary>         public DictionaryCache()
        {
            Store = new Dictionary<string, Medium>();
        }
        public void Add(string key,Func<object> func)
        {
            if (Store.ContainsKey(key)) {//修改,若是已經存在,再次添加時則採用其它線程                 var elem = Store[key];
                if (elem.IsUpdating)return;  //正在寫入未命中                 var th = new ThreadHelper(elem, func);//ThreadHelper將在下文說起,是向其它線程傳參用的
                var td = new Thread(th.Doit);
                td.Start();
            }
            else {//首次添加時可能也要讀取,因此要本線程執行                 Console.WriteLine("Begin first write");
                Store.Add(key, new Medium {IsPrimary = true, Primary =  func()});
                Console.WriteLine("End first write");
            }

        }
        /// <summary>         /// 讀取時所用的索引         /// </summary>         /// <param name="key"></param>         /// <returns></returns>         public object this[string key] {
            get {
                if (!Store.ContainsKey(key))return null;
                var elem = Store[key];
                if (elem.IsUpdated) {//若是其它線程更新完畢,則將主次轉置                     elem.IsUpdated = false;
                    elem.IsPrimary = !elem.IsPrimary;
                } 
                var ret = elem.IsPrimary ? elem.Primary : elem.Secondary;
                var b = elem.IsPrimary ? " from 1" : " form 2";
                return ret + b;
            }
        }
        Dictionary<string, Medium> Store { get; set; }
        public IEnumerator GetEnumerator() {
            return ((IEnumerable)Store).GetEnumerator();
        }
    }
}

這裏我只實現了插入一個緩存,以及讀取的方法。

我讀取緩存單元的邏輯是這樣的

image 

從2個不一樣緩存讀取固然是很容易了,可是比較複雜的就是向緩存寫入的過程:

image

這裏讀取數據以及寫入緩存時我使用了一個委託,在其它線程中僅在須要執行時纔會執行。

這裏除了首次寫入緩存佔用主線程時間(讀取要等待)之外,其它時間均可以無延時的讀取,實現了無縫的緩存。

但咱們在委託中要操做緩存的元素Medium,因此要傳遞參數進其它線程,因此我這裏使用了一個輔助類來傳遞參數進入其它線程:

using System;namespace CHCache {
    /// <summary>     /// 一個線程Helper,用於幫助多拋出線程時傳遞參數     /// </summary>     public class ThreadHelper {
        Func<object> Fun { get; set; }
        Medium Medium { get; set; }
        /// <summary>         /// 經過構造函數來傳遞參數         /// </summary>         /// <param name="m">緩存單元</param>         /// <param name="fun">讀取數據的委託</param>         public ThreadHelper(Medium m,Func<object> fun) {
            Medium = m;
            Fun = fun;
        }
        /// <summary>         /// 線程入口,ThreadStart委託所對應的方法         /// </summary>         public void Doit()
        {
            Medium.IsUpdating = true;
            if (Medium.IsPrimary) {
                Console.WriteLine("Begin write to 2.");
                var ret = Fun.Invoke();
                Medium.Secondary = ret;
                Console.WriteLine("End write to 2.");
            }
            else {
                Console.WriteLine("Begin write to 1.");
                var ret = Fun.Invoke();
                Medium.Primary = ret;
                Console.WriteLine("End write to 1.");
            }
            Medium.IsUpdated = true;
            Medium.IsUpdating = false;
        }
    }
}

這樣咱們就實現了在另個線程讀取數據的過程,這樣就在任什麼時候候讀取數據時都會無延時直接讀取了。

最後咱們寫一個主函數來測試一下效果

/* * http://www.cnblogs.com/chsword/ * chsword * Date: 2009-3-31 * Time: 16:53 */ using System;using System.Threading;namespace CHCache
{
    class Program
    {
        public static void Main(string[] args)
        {
            var cache = new DictionaryCache();
            Console.WriteLine("Init...4s,you can press the CTRL+C to close the console window.");
            while (true)
            {
                cache.Add("1", GetValue);
                Thread.Sleep(1000);
                Console.WriteLine(cache["1"]);
            }
        }
        /// <summary>         /// 獲取數據的方法,假設是從數據庫讀取的,費時約4秒         /// </summary>         /// <returns></returns>         static object GetValue()
        {
            Thread.Sleep(4000);
            return DateTime.Now;
        }
    }
}

獲得以下數據:

image

這樣就實現了平滑的讀取緩存數據而沒有任何等待時間

固然這裏還有些問題,好比說傳遞不一樣參數時的解決方法,可是因爲我僅是在一個統計時須要這種緩存提升性能,因此暫沒有考慮通用的傳參方式。

若是你們對這個話題感興趣,歡迎討論。

相關文章
相關標籤/搜索