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

最近在做一個WEB的數據統計的優化,但是由於數據量大,執行一次SQL統計要比較長的時間(一般700ms算是正常)。
正常的做法只要加個緩存就好了。
但是同時業務要求此數據最多1分鐘就要更新,而且這一分種內數據可能會有較多變化(而且原系統不太易擴展)。
也就是說緩存1分鐘就要失效重新統計,而且用戶訪問這頁還很是頻繁,如果使用一般緩存那麼用戶體驗很差而且很容易造成超時。
 

看到以上需求,第一個進入我大腦的就是從前做遊戲時接觸到的DDraw的雙緩衝顯示方式。
p_w_picpath
在第一幀顯示的同時,正在計算第二幀,這樣讀取和計算就可以分開了,也就避免了讀取時計算,提高了用戶體驗。
我想當然我們也可以將這種方式用於緩存的策略中,但這樣用空間換取時間的方式還是得權衡的,因爲並不是所有時候都值得這麼做,但這裏我覺得這樣做應該是最好的方式了。
注:爲了可以好好演示,本篇中的緩存都以IEnumerable的形式來存儲,當然這個文中原理也可以應用在WebCache中。
這裏我使用以下數據結構做爲存儲單元:
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; }
}
}
   有了這個數據結構我們就可以將數據實現兩份存儲。再利用一些讀寫策略就可以實現上面我們講的緩存方式。

整個的緩存我們使用如下緩存類來控制:
/*
* [url]http://www.cnblogs.com/chsword/[/url]
* 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();
}
}
}
這裏我只實現了插入一個緩存,以及讀取的方法。
我讀取緩存單元的邏輯是這樣的
p_w_picpath 
從2個不同緩存讀取當然是很容易了,但是比較複雜的就是向緩存寫入的過程:
p_w_picpath
這裏讀取數據以及寫入緩存時我使用了一個委託,在其它線程中僅在需要執行時纔會執行。
這裏除了首次寫入緩存佔用主線程時間(讀取要等待)以外,其它時間都可以無延時的讀取,實現了無縫的緩存。
但我們在委託中要操作緩存的元素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;
}
}
}
這樣我們就實現了在另個線程讀取數據的過程,這樣就在任何時候讀取數據時都會無延時直接讀取了。

最後我們寫一個主函數來測試一下效果
/*
* [url]http://www.cnblogs.com/chsword/[/url]
* 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;
}
}
}
得到如下數據:
p_w_picpath
這樣就實現了平滑的讀取緩存數據而沒有任何等待時間
當然這裏還有些問題,比如說傳遞不同參數時的解決方法,但是由於我僅是在一個統計時需要這種緩存提高性能,所以暫沒有考慮通用的傳參方式。
如果大家對這個話題感興趣,歡迎討論。
 
示例下載:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章