過去在ASP時代,若想將資料保存在Server端,我們可以用Session或Application(雖然在談Scalability時,會建議避用這類Stateful的設計,不過那又是另一個一千零一夜的故事了,這裡先不提)。在ASP.NET中,有另一個好用的選擇: Cache

比較起來,Cache比較像Application,因為它是所有的ASPX共享的空間,不屬於特定的Session;但它跟Application又有點不同: 它是"Cache",代表萬一記憶體不足時,它可是會被清掉的,所以我在寫Cache的邏輯時,多半會用以下的做法: (遇到記憶體不足時,CachItemPriority設得愈低,會愈先被放棄)

排版顯示純文字
if (Cache[key]==null) {
   //...略... 重新產生要被Cache的物件, 放入變數t
   Cache.Add(key, t, null, DateTime.Now.AddHours(1),
System.Web.Caching.Cache.NoSlidingExpiration,
System.Web.Caching.CacheItemPriority.High, null);
}
return Cache[key];

如此,就可以達到On-Demand重建的效果,任何Request一旦發現Cache中找不到資料或已過期,立刻重新產生要Cache的物件放入Cache中,造福後人。

要將資料放入Cache,可以用InsertAdd,二者最大的不同是Insert時會不論Cache中是否已有該Key值的CacheItem時,一律新增或覆寫為傳入的value;而Add在Cache中已存在該Key時,會直接傳回現有物件,不做更新。

跟Application比起來,Cache多了很多彈性,它可以設定Dependency(例如: 某個File一旦變動Cache內容就會自動被移除)、設定某個時間後移除、也可以設定無人使用後多久自動消失(這點又跟Session的20分鐘保存期限很像),還可以指定當Cache被移除時要呼叫的函數(CacheItemRemovedCallback)。不過,一般我倒很少用到那麼複雜的功能,多半只用到兩個參數: absoluteExpiration與slidingExpiration。

二者的使用時機有點不同,absoluteExpiration我多會用在常被查詢的龐大資料表,例如: 系統中常常要將員工編號換成員工姓名,若每次都去資料庫現場查,勞民傷財。所以我會讀取一次,轉成Dictionary<string, string>或Hashtable後放入Cache,並設上一或兩個小時後失效。反正員工資料也不會分分秒秒都在變化,最壞的情況是修改員工資料後,要等上兩個小時後才會生效。不過實務上為了避免發生某位大長官大為了某個臭工程師在效能上的堅持苦等兩小時,我多半還是會設計清除該Cache的功能,必要時呼叫一下,即刻將Cache移除,資料馬上就更新了。使用絕對過期時間時,除了指定DateTime物件(我通常是用DateTime.Now.AddHours()設時間)給absoluteExpiration參數外,slidingExpiration要設為System.Web.Caching.NoSlidingExpiration。

用到slidingExpiration多半是要模仿Session物件的效果,只要有繼續存取Cache[key],物件就會一直保留在Cache中,直到閒置超過slidingExpiration指定的時間為止。不過,Cache的Scope是全Process,並不限於某個User Session,如果我把Key值取為"AAA",豈不等於全部User都把資料放進同一個置物櫃中,保證天下大亂。所以一定要為每個Session取個唯一的Key值,ASP.NET的SessionID,Logon User都可以被考量,但我還是最愛Guid,Page_OnLoad生一個出來,放進ViewState中,後面就好辦了。

排版顯示純文字
private string getSessionKey() {
    if (ViewState["SessionKey"]==null)
        ViewState["SessionKey"]=Guid.NewGuid().ToString();
    return ViewState["SessionKey"].ToString();
}
 
private void saveToCache(object obj) {
    Cache.Insert(getSessionKey(), obj, null, 
System.Web.Caching.Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(20), 
System.Web.Caching.CacheItemPriority.High, null);
}

以上就是我最常應用Cache的做法,大家如果有什麼獨到的心得,也歡迎拿出來分享一下。


Comments

# by Jeffrey

To elleryq: 真是好文章,用delegate回補資料的做法很酷! 謝謝分享

# by Andrew

大大您的文章讓我受益良多, 但關於第2種cache的作法, 我想像會是每一個session進來, server都會建立各個session專屬的cache物件, 那這樣會不會反而浪費server的資源而失去cache的原意呢?? 謝謝

# by Jeffrey

to Andrew, 第二者寫法適用的時機的確就是每個Session自己要有一份專屬的資料(實務上也挺常見的),互不混用,至於此時為何不用直接用Session物件而要用Cache? 我想一個小小的差別是: Cache可以在記憶體不足時被捨棄(所以叫用時一定要考慮它是null而重新生成的邏輯),而Session則就算記憶體已吃緊還是會一直佔用者空間,當然上,實務上也許真正能彰顯這份行為差異的場合並不太多就是了。

# by Slash

謝謝大大的blog,我常逛喔! 多文章都是我在實作上會出現同樣的問題狀況,不知可否跟大大要一下 msn 加入好友?對了,在這篇文章中您有一個地方有一點點小筆誤 System.Web.Caching.NoSlidingExpiration 要改 System.Web.Caching.Cache.NoSlidingExpiration

# by Jeffrey

to Slash, 謝謝指正,筆誤真的是我邁向專業作家之路的絆腳石呀~~~ 文章上的問題歡迎在Blog上留言,另外,也可透過Plurk找到我,Plurk的即時性更高一些。不過有時工作較忙,不一定能立刻回應還請見諒。

# by 當麻許

學到~感謝黑大

Post a comment