ASP.NET 緩衝: 技術及最佳實踐

摘要:
       ASP.NET 提供了三種主要形式的緩衝: 頁面級輸出緩衝, 用戶控件及輸出緩衝 (或片斷型緩衝), 以及 Cache 編程接口. 輸出緩衝和片斷緩衝有着非常易於實現的優點, 並且在許多情況下已經足夠好用. Cache 編程接口提供了額外的靈活性, 可以用來使得應用程序的每一層都能充分利用到緩衝技術.

       在ASP.NET 的衆多特性中, 緩衝支持無疑是我最喜歡的, 和所有其它ASP.NET的特性比起來, 它對應用程序的性能有着最大的潛在影響, 而且它使得開發者願意接受使用相對重量級的控件如 DataGrid 來建站所帶來的額外開銷, 卻不用害怕性能會愛到太大的影響. 爲了在你的應用程序中看到緩衝所帶來的最多的好處, 你應該考慮在程序的所有層中實現緩衝的方法.

Steve 的緩衝技巧

Cache Early,  Cache Often

 
      在你應用程序的每一層實現緩衝. 給數據層, 業務邏輯層, 以及界面或輸出層添加緩衝支持. 在整個應用程序中以聰明的方式來實現緩衝, 你可以成功地獲得巨大的性能提升.

Caching Hides Many Sins
 
       緩衝可以是一個很好的辦法來取得足夠好的性能而不必要花費很多的時間或分析. 如果你能通過緩存30秒輸出來得到你想需要的性能, 而不是花費一天或一週的時間來試圖優化你的代碼或數據庫, 那麼使用緩衝吧 (假設 30 秒前的舊有數據沒有問題). 緩衝是那些能夠用20%的工作換取80%的收益的技術之一, 因此它應該是你試圖提升性能的首選. 最後, 糟糕的設計很可能困擾着你, 所以你當然應該試圖正確地設計你的應用. 但如果你僅需要現在獲得足夠好的性能, 緩衝可能是非常棒的, 它爲你爭取了時間, 使得你可以在晚些時候重構你的應用.

頁面級輸出緩衝
 
        最簡單形式的緩衝, 輸出緩衝簡單地在內存中維持一份HTML的拷貝, 它是發送過的對一個請求的應答. 後續的請求將會被髮送這個緩存的
輸出直到緩存到期, 結果可以獲取非常大的潛在性能提升 (取決於創建原始的頁面輸出需要多大的開銷--發送緩衝的輸出總是很快的而且相對
穩定).

實現

 要實現頁面輸出緩衝, 只要簡單地在頁面中添加 OutputCache 指示符.

 <%@ OutputCache Duration="60" VaryByParam="*" %>

       這個指示符和其它的指示符一起應該被放在ASPX頁面的頂部, 在任何輸出之前. 它支持5個屬性(或參數), 其中2個是必須的.

Duration:   必須. 以秒錶示的時間, 頁面應該被緩衝. 必須是一個正數.
Location:   指定輸出應該在哪裏被緩衝. 如果指定該屬性, 它的值必須是 Any, Client, Downstream, None, Server 或  ServerAndClient 之一.
VaryByParam: 必須. 請求中變量的名字, 這個會導致不同的緩衝入口, 可以用 "none" 表示沒有區別, "*" 用來爲每一套不同的 變量創建新的緩衝入口. 用 ";" 來劃分變量.
VaryByHeader: 基於指定的頭字段的變化來改變緩衝入口.
VaryByCustom: 允許在 global.asax 中指定自定義的變化.如 "Browser".
 
       大多數的情形可以由必須的 Duration 及 VaryByParam 來處理. 比方說, 如果你有一個產品目錄, 它允許用戶基於 CategoryID 和一個頁面變量來查看相應目錄的頁面, 你可以緩衝它一段時間 (一個小時應該是可以接受的, 除非這些產品時刻在變化, 因此一個3600秒的 duration) 用一個值爲 "categoryID; page" 的 VaryByParam 屬性. 這會爲每個目錄的目錄頁面創建不同的緩衝入口. 每一個入口會從它的第一次請求開始持續一個小時.

         VaryByHeader 和 VaryByCustom 主要被用來根據訪問用戶的不同允許頁面外觀或內容的個性化. 也許相同的頁面要在瀏覽器和手機上呈現, 需要緩衝不同的版本. 或者頁面對IE進行了優化但需要能夠在Netscape或 Opera 上稍微地降低要求 (而不是崩潰).  最後一個示例我們將展現一個怎樣做的例子.

例子: VaryByCustom 支持瀏覽器個性化
        要能夠爲每一個瀏覽器區分緩衝入口, VaryByCustom 可被設爲一個 "browser" 值. 這個功能內建在緩衝模塊裏, 會爲每一個瀏覽器名及主版本號插入不同緩衝版本的頁面.

 <%@ OutputCache Duration="60" VaryByParam="None" VaryByCustom="browser" %>

片斷緩衝, 用戶控件輸出緩衝
 
       在很多情況下, 緩衝整個頁面並不可行, 因爲頁面的特定部分是爲不同用戶定製的. 但是, 可能頁面的其它部分對於整個應用程序來說是通用的. 這些最適合被緩衝, 使用片斷緩衝和用戶控件緩衝. 菜單和其它而已元素, 尤其是從一個數據源動態產生的, 應該用這種技術來緩衝.  如果需要, 緩衝的控件可被配置爲基於子控件 (或其它屬性) 或任何其它頁面級輸出緩衝支持的變更而不同. 許許多多使用相同控件的頁面
也可以共享這些控件的緩衝入口, 而不是爲每一個頁面分別保存一個緩衝的版本.

實現

        片斷緩衝使用和頁面級輸出緩衝一樣的語法, 但是應用在一個用戶控件上 (.ascx文件) 而不是一個web窗體 (.aspx文件). 除了 Location屬性外, 所有web窗體支持的 OutputCache 指示符屬性同樣被用戶控件支持. 用戶控件來支持一個 OutputCache 屬性稱爲 VaryByControl, 它會根據該控件成員的值來變更緩衝 (典型情況是一個頁面上的控件, 如 DropDownList),  如果指定了 VaryByControl ,  VaryByParam 將被忽略. 默認情況下, 每一個頁面上的用戶控件會被分別緩衝, 但是, 如果用戶控件在應和程序中不同的頁面間沒有變化而且在所有這些頁面中使用同樣的命名, 可以在其上應用 Shared="true" 參數, 這會使該用戶控件的緩衝版本被所有引用該控件的頁面使用.

例子:

 <%@ OutputCache Duration="60" VaryByParam="*" %>

      這會將用戶控件緩衝60秒, 並且爲每一個不同查詢字符串 query string , 每一個引用該控件的頁面, 分別創建緩衝入口.
 
 <%@ OutputCache Duration="60" VaryByParam="none" VaryByControl="CategoryDropDownList" %>

     這將會緩衝這個用戶控件60秒, 並且爲 CategoryDropDownList 控件的每一個不同值, 每一個引用該控件的頁面, 分別創建一個緩衝入口,

 <%@ OutputCache Duration="60" VaryByParam="none" VaryByCustom="browser" Shared="true" %>

      最後, 這個示例緩衝該用戶控件60秒, 並且爲每一個不同的瀏覽器名和主版本號創建一個緩衝入口. 每個瀏覽器的緩衝入口會被所有引用該控件的頁面共享 ( 只要所有頁面用同樣的 ID 來引用它).

緩衝編程接口中, 使用緩衝對象

       頁面和用戶控件級輸出緩衝可能是一個快速和簡便的方法來提升你的站點性能, 但是緩衝的真正的靈活性和強大的功能是通過 Cache對象展示的. 使用 Cache 對象, 你可以存儲任何可序列化的數據對象, 並且控制緩衝入口如何基於一個或多個依賴項的組合來到期 (expire).  這些依賴項包括從數據項緩衝以來經過的時間, 從上次訪問以來經過的時間, 對文件或文件夾的更改, 對其它緩衝數據項的更改, 或對數據庫
中特定表的更改.

在 Cache 中存儲數據

     在 Cache 中存儲數據的最簡單的方法就是直接分配, 使用一個 key, 就象哈希表或字典對象一樣:
 
 Cache["key"] = "value";

       這將會把這個數據項存儲在緩存中而不包含任何依賴項, 因此除非緩存引擎要爲給其它額外緩衝對象騰出空間而移除它, 否則它不會過期. 要明確包含依賴項, 我們使用 Add() 或 Insert() 方法. 每一個方法都有一些重載版本. 它們之間僅有區別在於 Add() 返回一個緩衝對象的引用, 而 Insert() 不返回任何值 ( void in c#, Sub in VB).

例子

 Cache.Insert ("key", myXMLFileData, new System.Web.Caching.CacheDependency (Server.MapPath ("users.xml")));

       這段代碼將把一個文件中的 xml 數據插入到緩存中, 在後續的請求中就不需要從這個文件中讀取.  CacheDependency 確保當這個文件改變時, 緩衝會馬上過期, 以允許從文件中拉出最新的數據並重新緩衝. 還可以指定一個文件名的數組, 如果緩衝的數據依賴於多個文件.

 Cache.Insert ("dependentkey", myDependentData,
    new System.Web.Caching.CacheDependency (new String[] {}, new String[] {"key"} ));
 
       這個例子要插入一份數據, 它依賴於第一份數據的存在 (鍵值爲"key"). 如果在緩存中沒有鍵值爲"key"的入口, 或者與該鍵值關聯的數據項過期或更新了, 那麼這個稱爲 "dependentkey" 的緩存入口將過期.

 Cache.Insert ("key", myTimeSensitiveData, null, DateTime.Now.AddMinutes(1), TimeSpan.Zero);

絕對到期時間: 這個例子將該時間敏感的數據緩衝一分鐘, 到時候這個緩衝就過期. 注意絕對過期時間和變化的過期時間不能一起用.

 Cache.Insert ("key", myFrequestlyAccessedData, null, System.Web.Caching.Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(1));

        變化的過期時間: 這個例子緩衝一些頻繁訪問的數據. 數據將保持在緩衝中直到無任何對象引用它達到一分鐘以上. 注意它和絕對過期時間不能混用.

更多選項

       除了在上面提到的依賴以外, 我們還可以指定數據項的優先級 (從低到高至不可移除, 在System.Web.Caching.CacheItemPriority枚舉中定義) 和一個 CacheITemRemovedCallback 方法, 當數據項在緩衝中到期時調用. 大多情況下, 默認優先級已經足夠--讓緩衝引擎來決定怎樣做更好及處理緩衝的內存管理. CacheItemRemovedCallback 選項允許一些有趣的結果, 但在實踐中, 幾乎不使用它. 但是, 爲了演示這個技術, 我給出一個用法的示例:

CacheItemRemovedCallback 示例

 CacheItemRemovedCallback callback = new CacheItemRemovedCallback (OnRemove);
 Cache.Insert("key",myFile,null,  Cache.NoAbsoluteExpiration, TimeSpan.Zero, CacheItemPriority.Default, callback);
  . . .
 public static void OnRemove(string key,  object cacheItem, CacheItemRemovedReason reason)
 {
  AppendLog("The cached value with key '" + key + "' was removed from the cache.  Reason: " + reason.ToString());
 }

 
        該示例將數據在緩衝中過期的原因記入日誌, 使用在 AppendLog() 方法中定義的邏輯. 當數據項從緩衝中移除時記錄它並註明原因可以允許你測定你是否有效地使用了緩衝, 或者也許你需要增加服務器上的內存. 注意這個回調方法是一個靜態方法, 推薦使用這種方法,  因爲如果不這樣, 將有一個保存這個回調方法的對象實例在內存中, 而靜態方法則不需要. 這個特性的一個可能的用處是在後臺刷新緩衝的數據, 這樣用戶永遠不需要等待數據的產生, 但數據保持相對較新. 但在實踐中很不
幸的是在當前版本的緩衝編程接口中, 這不是工作得很好, 因爲回調方法不在數據項從緩衝中移除之前激發或完全執行. 這樣當用戶將頻繁發出請求試圖訪問緩衝的值, 卻發現它爲空, 而且必須要等待它重新產生. 在ASP.NET的未來版本中, 我希望看到一個額外的回調方法, 可能叫 CachedItemExpiredButNotRemovedCallback, 它將在緩衝數據項被移除之前完全執行.

緩衝數據引用模式

       不論何時試圖訪問緩衝中的數據, 應該有數據可能已經不再那裏的思想準備, 這樣下面的模式應該對訪問緩衝數據來說是通用的. 這個場景中, 我們假設被緩衝的數據是一個 DataTable.

 public DataTable GetCustomers(bool BypassCache)
 {
    string cacheKey = "CustomersDataTable";
    object cacheItem = Cache[cacheKey] as DataTable;
    if((BypassCache) || (cacheItem == null))
    {
       cacheItem = GetCustomersFromDataSource();
       Cache.Insert(cacheKey, cacheItem, null,
       DateTime.Now.AddSeconds(GetCacheSecondsFromConfig(cacheKey),
       TimeSpan.Zero);
    }
    return (DataTable)cacheItem;
 }

 關於這個模式有幾點要指出:

 1. 值, 比如 cacheKey, cacheItem 以及 cache duration 被定義了一次且僅僅一次.
 2, 如果需要的話, 緩衝可以被忽略--比方說, 剛剛註冊了一個新客戶並重定向到一個客戶列表, 很可能忽略緩衝並用最新數據重新產生列表是最好的, 這樣會包含這個新加入的客戶.
 3. 緩衝僅被訪問一次. 這會有一些性能上的好處而且保證空指針異常不會發生, 因爲數據項在第一次檢查的時候被呈現, 但在第二次檢查之前已經過期.
 4.  該模式使用強類型檢查. c#中的 "as" 運算符試圖將 object 轉換成一個類並且失敗後簡單返回一個null.
 5.  持續時間存放在一個配置文件中. 所有緩衝依賴項, 無論是基於文件, 基於時間或其它的, 理想情況下應該被放在一個配置文件中, 這樣可以更方便地進行更改及性能測試. 我還建議指定一個默認的緩衝持續時間, 而且 GetCacheSecondsFromConfig()
方法在當前使用的cacheKey沒有指定持續時間時使用默認的時間.

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