最近在作一個WEB的數據統計的優化,可是因爲數據量大,執行一次SQL統計要比較長的時間(通常700ms算是正常)。數據庫
正常的作法只要加個緩存就行了。緩存
可是同時業務要求此數據最多1分鐘就要更新,並且這一分種內數據可能會有較多變化(並且原系統不太易擴展)。數據結構
也就是說緩存1分鐘就要失效從新統計,並且用戶訪問這頁還非常頻繁,若是使用通常緩存那麼用戶體驗不好並且很容易形成超時。函數
看到以上需求,第一個進入我大腦的就是從前作遊戲時接觸到的DDraw的雙緩衝顯示方式。性能
在第一幀顯示的同時,正在計算第二幀,這樣讀取和計算就能夠分開了,也就避免了讀取時計算,提升了用戶體驗。測試
我想固然咱們也能夠將這種方式用於緩存的策略中,但這樣用空間換取時間的方式仍是得權衡的,由於並非全部時候都值得這麼作,但這裏我以爲這樣作應該是最好的方式了。優化
注:爲了能夠好好演示,本篇中的緩存都以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(); } } }
這裏我只實現了插入一個緩存,以及讀取的方法。
我讀取緩存單元的邏輯是這樣的
從2個不一樣緩存讀取固然是很容易了,可是比較複雜的就是向緩存寫入的過程:
這裏讀取數據以及寫入緩存時我使用了一個委託,在其它線程中僅在須要執行時纔會執行。
這裏除了首次寫入緩存佔用主線程時間(讀取要等待)之外,其它時間均可以無延時的讀取,實現了無縫的緩存。
但咱們在委託中要操做緩存的元素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; } } }
獲得以下數據:
這樣就實現了平滑的讀取緩存數據而沒有任何等待時間
固然這裏還有些問題,好比說傳遞不一樣參數時的解決方法,可是因爲我僅是在一個統計時須要這種緩存提升性能,因此暫沒有考慮通用的傳參方式。
若是你們對這個話題感興趣,歡迎討論。