程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 無縫的緩存讀取:雙存儲緩存策略

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

編輯:關於.NET

最近在做一個WEB的數據統計的優化,但是由於數據量大,執行一次SQL統計要比較長的時間(一般700ms算是正常)。

正常的做法只要加個緩存就好了。

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

也就是說緩存1分鐘就要失效重新統計,而且用戶訪問這頁還很是頻繁,如果使用一般緩存那麼用戶體驗很差而且很容易造成超時。

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

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

我想當然我們也可以將這種方式用於緩存的策略中,但這樣用空間換取時間的方式還是得權衡的,因為並不是所有時候都值得這麼做,但這裡我覺得這樣做應該是最好的方式了。

注:為了可以好好演示,本篇中的緩存都以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; }
    }
}  

有了這個數據結構我們就可以將數據實現兩份存儲。再利用一些讀寫策略就可以實現上面我們講的緩存方式。轉

整個的緩存我們使用如下緩存類來控制:

/*
 * 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;
        }
    }
}

得到如下數據:

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

當然這裡還有些問題,比如說傳遞不同參數時的解決方法,但是由於我僅是在一個統計時需要這種緩存提高性能,所以暫沒有考慮通用的傳參方式。

如果大家對這個話題感興趣,歡迎討論。

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