帶你走進緩存世界(4):緩存之緩

        緩存二字,從字面上分爲兩塊:“緩”與“存”。上節我們提到的緩存原理,其實是在講的一個“存”字,如何存取。大致回顧下是key對應的hashcode,根據hashcode作爲數組下標來存取,因爲存在hash衝突,速度雖達不到O(1),但也是非常之快。今天就說下“緩”的策略。

  緩,便意味着“暫時”的意思,過一段時間就不再存在或被替換掉了,所以我們要說的其實是緩存的過期策略。在緩存入門篇中,主要提到了Cache類的Insert的方法,其中的幾個變化的參數寓意着各種緩存策略,有具體依賴的有按時間的,一一來看。

  按過期時間緩存
  這種緩存策略最爲簡單,只要判斷當前時間是否超過了指定的過期時間就remove掉該緩存項即可,一般用於不影響大礙的數據,比如論壇帖子列表,熱門板塊會更新極其頻繁,緩存起來最爲合適。但是又不能不更新緩存,不然有人發帖和回帖就看不到了,但可以緩存個一兩分鐘,兩分鐘後自動過期,重新加載新的列表,這樣就不用管了,所以這種緩存策略更傾向於“不用管”的緩存。既然如此,那麼我們就自己寫一個按時間過期的緩存類吧。下面的這個類非常基礎:

    /// <summary>
    /// 按時間緩存類
    /// </summary>
    public class CacheByDateTime<TKey,TValue>
    {
        /// <summary>
        /// 內部緩存項
        /// </summary>
        class CacheItem
        {
            /// <summary>
            /// 緩存的值
            /// </summary>
            public TValue value { get; set; }
            /// <summary>
            /// 過期時間
            /// </summary>
            public DateTime dateTime { get; set; }
        }

        /// <summary>
        /// 緩存數據詞典
        /// </summary>
        private readonly Dictionary<TKey, CacheItem> _dict;

	//爲了線程安全,需要對dict的操作加鎖
        private static readonly object LockDict = new object();

        public CacheByDateTime()
        {
            _dict = new Dictionary<TKey, CacheItem>();
        }
        /// <summary>
        /// 添加一個緩存
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <param name="dateTime">過期時間</param>
        public void Add(TKey key, TValue value, DateTime dateTime)
        {
            lock (LockDict)
            {
                if (_dict.ContainsKey(key))
                {
                    _dict[key].value = value;
                    _dict[key].dateTime = dateTime;
                }
                else
                {
                    _dict.Add(key, new CacheItem { value = value, dateTime = dateTime });
                }
            }
        }
        /// <summary>
        /// 獲取緩存
        /// </summary>
        public TValue Get(TKey key)
        {
            if (_dict.ContainsKey(key))
            {
                var val = _dict[key].value;
		//判斷緩存項是否過期
                if (_dict[key].dateTime > DateTime.Now)
                {
                    return val;
                }
                else
                {
                    Remove(key);
                    return val;//這裏可以酌情是否返回Value,因爲畢竟可以省去一次查詢
                }
            }
            return default(TValue);
        }
        /// <summary>
        /// 移除緩存
        /// </summary>
        public void Remove(TKey key)
        {
            lock (LockDict)
            {
                if (_dict.ContainsKey(key))
                {
                    _dict.Remove(key);
                }
            }
        }
    }

        按間隔時間緩存
        這個相對上面的絕對過期時間來說更有趣一些,他的策略是隻要被訪問,就延遲該緩存的絕對過期時間(間隔時間比如是5分鐘就延長5分鐘)。這種過期策略似乎十分精明,但對緩存的數據類型也是極其講究,這種策略一般來緩存什麼合適呢?如果說緩存永不過期的數據最爲合適,但不存在這樣的數據,像網站的配置這種數據極少改動,但訪問量巨大,如果用這種緩存策略,不管管理員怎麼修改配置,估計這緩存都是更新不了了,反而用上面的緩存合適,而像文章內容這種數據,訪問的隨機性比較大,拿捏不準啥時候過期,但文章內容極少會被更新,而網站的訪問量基本上又屬內容頁比較大,所以這種緩存緩存文章內容比較合適。可以有效的延長熱門內容的過期時間,而冷門的文章自然而言就自動過期了。具體的代碼實現只需要在上面的類的Add方面做些改動就可實現:
        /// <summary>
        /// 添加一個緩存
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <param name="timeSpan">間隔時間</param>
        public void Add(TKey key, TValue value, TimeSpan timeSpan)
        {
            lock (LockDict)
            {
                if (_dict.ContainsKey(key))
                {
                    _dict[key].value = value;
                    _dict[key].dateTime.Add(timeSpan);
                }
                else
                {
                    _dict.Add(key, new CacheItem { value = value, dateTime = DateTime.Now.Add(timeSpan) });
                }
            }
        }

        依賴項緩存
        依賴緩存相對以上兩個來說是非常複雜的處理過程,比如文件依賴,會有相應的監測程序(FileMonitor)來管理dependency對象。這裏我們便不講解,瞭解其用處即可,着實因爲太過複雜。有興趣的可以看.Net源碼。


        LRU(Least Recently Used)緩存
        從名字便知其意,其主要用於限定容量(比如內存大小或緩存數量)的緩存,需要在緩存容器滿了之後踢出過期緩存的策略,是使用次數最少或很久沒使用的緩存項策略。
        實現原理一般使用鏈表方式把所有緩存項連起來,每當有新的緩存進入則把緩存放入鏈表前端,如果緩存被使用則把他提到鏈表前端,那麼沒被使用的將慢慢趨於鏈表後端,所以當容量滿了以後,就優先移除鏈表末尾的緩存項。當然,也有其他更爲複雜的過期策略,比如同時使用緩存時間。雖然此策略和上面的按時間間隔延長緩存有點相像,但這個更側重於緩存容器大小的管理,畢竟內存是有限的,此策略多用於公共緩存服務。下面的類是個簡單的LRU實現,只限定的緩存的長度並沒有大小限制,如果要做大小限制則需要計算每一個value的大小。
 

    /// <summary>
    /// LRUCache
    /// </summary>
    public class LRUCache<TKey,TValue>
    {
        /// <summary>
        /// 緩存項
        /// </summary>
        class CacheItem
        {
            public TKey Key { get; set; }
            public TValue Value { get; set; }
            public CacheItem Left { get; set; }
            public CacheItem Right { get; set; }

            public CacheItem(TKey key, TValue value)
            {
                Key = key;
                Value = value;
            }
        }

        private readonly static object LockDict = new object();

        private readonly IDictionary<TKey, CacheItem> _dict;

        public int Length { get; private set; }

        public LRUCache(int maxLength)
        {
            _dict = new Dictionary<TKey, CacheItem>();
            Length = maxLength;
        }

        //鏈表頭部
        private CacheItem _first;
        //鏈表末端
        private CacheItem _last;

        public bool HasKey(TKey key)
        {
            return _dict.ContainsKey(key);
        }

        /// <summary>
        /// 添加一個緩存項
        /// </summary>
        public void Add(TKey key, TValue value)
        {
            var item = new CacheItem(key, value);

            lock (LockDict)
            {
                //如果沒有緩存項,則item既是first也是last
                if (_dict.Count == 0)
                {
                    _last = _first = item;
                }

                //如果只有一個緩存項,則item是first,first和last變爲last
                else if (_dict.Count == 1)
                {
                    _last = _first;
                    _first = item;

                    _last.Left = _first;
                    _first.Right = _last;
                }
                else
                {
                    //item爲first,之前的前端向後移位
                    item.Right = _first;
                    _first.Left = item;
                    _first = item;
                }

                //如果超過的鏈表長度
                if (_dict.Count >= Length)
                {
                    //斷開last並移除
                    _last.Left.Right = null;
                    _dict.Remove(_last.Key);
                  
                    _last = _last.Left;
                }

                //將item放入dict
                if (_dict.ContainsKey(key))
                    _dict[key] = new CacheItem(key, value);
                else
                    _dict.Add(key, new CacheItem(key, value));
            }
        }

        /// <summary>
        /// 獲取一個緩存項
        /// </summary>
        public TValue Get(TKey key)
        {
            if (!_dict.ContainsKey(key))
            {
                return default(TValue);
            }

            var item = _dict[key];

            lock (LockDict)
            {
                if (_dict.Count == 1)
                {
                    return item.Value;
                }

                //如果item左側有緩存項,則將左側的緩存指向item的右側
                if (item.Left != null)
                {
                    item.Left.Right = item.Right;
                }
                else
                {
                    //否則說明item是first
                    return item.Value;
                }

                //如果item右側有緩存項,則將右側的緩存指向item的左側
                if (item.Right != null)
                {
                    item.Right.Left = item.Left;
                }
                else
                {
                    //否則說明item是last
                    //將last的左側的右側斷開,讓其成爲last
                    _last.Left.Right = null;
                    _last = _last.Left;
                }
                //斷開item的左側,讓item成爲first,讓first成爲item的右側項
                item.Left = null;
                item.Right = _first;
                _first.Left = item;
                _first = item;
            }
            return item.Value;
        }

        public void Remove(TKey key)
        {
            if (!_dict.ContainsKey(key))
            {
                return;
            }

            var item = _dict[key];

            lock (LockDict)
            {
                //如果item左側有值,則將左側的右側指向item的右側
                if (item.Left != null)
                {
                    item.Left.Right = item.Right;
                }
                else
                {
                    //否則item則是first,所以將item的右側賦值給first
                    _first = item.Right;
                }

                //如果item的右側有值,則將item的右側的左值指向item的左側
                if (item.Right != null)
                {
                    item.Right.Left = item.Left;
                }
                else
                {
                    _last = item.Left;
                }

                _dict.Remove(key);
            }
        }

    }



以上提到的是我們常用的幾種緩存策略,當然還有其他的策略,我們後面也會提到。今天就先到這吧。


發佈了42 篇原創文章 · 獲贊 191 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章