緩,便意味着“暫時”的意思,過一段時間就不再存在或被替換掉了,所以我們要說的其實是緩存的過期策略。在緩存入門篇中,主要提到了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);
}
}
}
以上提到的是我們常用的幾種緩存策略,當然還有其他的策略,我們後面也會提到。今天就先到這吧。