【原文地址】 Tip/Trick: Implement "Donut Caching" with the ASP.NET 2.0 Output Cache Substitution Feature
【原文發表日期】 Tuesday, November 28, 2006 12:17 AM
一些背景:
ASP.NET中一個強大無比,但往往未被充分利用的功能是它豐富的緩衝設施。ASP.NET的緩衝功能允許你在服務端避免為來自用戶端的每一個新請求做重複的工作,而是,你可以一次產生HTML內容或資料結構,然後在伺服器端ASP.NET中緩衝或儲存其結果,在以後的web請求中重用這些結果。這可以極大地提高你應用的效能,降低對象資料庫這樣的關鍵後台資源的負載。
Steve Smith 幾年前曾在MSDN上寫過一篇很好的關於ASP.NET 1.1 中緩衝的文章,討論了ASP.NET 1.1 緩衝功能的一些基本知識,並且對如何使用它們提供了一個很好的總結。如果你以前從沒有用過ASP.NET 緩衝的話,我建議你先讀一下這篇文章,並對其中的每個特性都嘗試一下。我也非常建議你觀看一下ASP.NET 2.0 免費錄影系列中的這個15分鐘的關於ASP.NET緩衝的“How Do I”錄影,這個錄影實戰示範了ASP.NET 緩衝。
ASP.NET 2.0添加了2個非常重要的改進,使得緩衝功能更加完善:
1) 對SQL緩衝失效的支援 - 這允許你在緩衝的頁面或資料結構所依賴的資料表或記錄行被更新時,使緩衝內容自動失效然後重建緩衝內容。例如,你可以在一個電子商務網站上輸出緩衝你所有的產品列表網頁,然後確信在資料庫中的產品價格一旦有所變動,這些網頁就會在下一個請求時重建,這樣就不會向使用者顯示到期的價格資料了。
2) 輸出緩衝的替換 - 這個奇妙的特性允許你實現我有時稱之為“甜圈緩衝(donut caching)” 的功能,在這裡,你輸出快取頁面面上的所有東西,但除了幾個包含在快取區域內的動態地區外。這允許你更積極地實現整頁輸出緩衝,不用為了實現局部頁面緩衝而把你的頁面分成多個.ascx使用者檔案。下面這個技巧/訣竅指南更好地解釋了這個特性的促動因素以及其實現。
實戰中的情境:
你要在你的網站上實現一個產品列單網頁,在上面列出在某個指定產品分類下的所有產品。你也想要輸出緩衝這個網頁,這樣,你就不用在每次請求時都訪問資料庫。你在Products.aspx 網頁的頂部用聲明的方式添加一個 <%@ OutputCache %> 指令就可以很輕鬆地達成這個目的,該網頁上包含一個綁定到從你的中介層返回的產品資料的 <asp:datalist> 控制項。
注意下面該網頁是如何設定輸出緩衝它的內容 100,000 秒或者直到northwind資料庫中的 products資料表為新的價格資料所更新為止,在後面這個情形下,下一個請求時,它就會重建頁面。OutputCache 指令還有一個VaryByParam 屬性,它告訴 ASP.NET 為每個獨特的categoryID 單獨儲存一份快取頁面面。譬如,Products.aspx?categoryId=1,Products.aspx?categoryId=2等等各有一個單獨的快取頁面面。
Products.aspx:
<%@ Page Language="VB" MasterPageFile="~/Site.master" AutoEventWireup="false" CodeFile="Products.aspx.vb" Inherits="Products" %>
<%@ OutputCache Duration="100000" VaryByParam="CategoryID" SqlDependency="northwind:products" %>
<asp:Content ID="Content1" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
<div class="catalogue">
<asp:DataList ID="DataList1" RepeatColumns="2" runat="server">
<ItemTemplate>
<div class="productimage">
<img src="images/productimage.gif" />
</div>
<div class="productdetails">
<div class="ProductListHead">
<%#Eval("ProductName")%>
</div>
<span class="ProductListItem">
<strong>Price:</strong>
<%# Eval("UnitPrice", "{0:c}") %>
</span>
</div>
</ItemTemplate>
</asp:DataList>
<h3>Generated @ <%=Now.ToLongTimeString()%></h3>
</div>
</asp:Content>
Products.aspx.vb:
Partial Class Products
Inherits System.Web.UI.Page
Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
Dim products As New NorthwindTableAdapters.ProductsTableAdapter
DataList1.DataSource = products.GetProductsByCategoryID(Request.QueryString("categoryId"))
DataList1.DataBind()
End Sub
End Class
瀏覽器訪問時,從伺服器端返回下面這個頁面:
注意,頁面底部的時間戳記每隔100,000秒,或者當products資料表裡的價格資料被更新時,才會被更新。它將會被緩衝起來以應付所有其他的 HTTP 要求,允許我們在生產伺服器上每秒處理1000個請求,避免了不必要的資料庫訪問,從而使得訪問的速度極快。
問題:
我們在上面的例子中將會遇到的一個問題,跟我們在頁面右上方輸出的歡迎訊息和使用者名稱字(在上面紅圈中)有關。目前這是在我們的Site.Master母板頁檔案中通過使用一個新的ASP.NET 2.0 <asp:loginname> 控制項來產生的,象這樣:
<div class="header">
<h1>Caching Samples</h1>
<div class="loginstatus">
<asp:LoginName FormatString="Welcome {0}!" runat="server" />
</div>
</div>
我們將撞上的問題是,因為我們給我們的頁面加了整頁輸出緩衝,第一個訪問網站的使用者的名字將被儲存到頁面的緩衝輸出中,這意味著,在預設情形下,在初始請求之後的 100,000 秒之內訪問網站的使用者將收到一個錯誤的歡迎訊息,更糟糕的是,顯示的是錯誤的使用者名稱字!
解決方案:
有2個方法可以解決這個問題。
第一個方案是將整個頁面做成動態,即去除頂層的 <%@ OutputCache %> 指令,對頁面的內容重構,把所有可快取的內容都封裝在ASP.NET使用者控制項中,這些使用者控制項是通過 .ascx 檔案來實現的。然後你在這些使用者控制項的每一個檔案的頂部添加 <%@ OutputCache %> 指令,使得它們可以單獨緩衝。這避免了每次請求都需要訪問資料庫,確保了使用者名稱字總是正確地輸出的,因為使用者名稱字不在緩衝的使用者控制項地區裡。這個方法目前在ASP.NET 1.1 裡就可行,當然,在ASP.NET 2.0依然行之有效。
但這個方案的缺點是,為使緩衝可行,它要求我們重構我們頁面的編碼和布局。但假如我們在頁面上只有幾個地方我們想要保持動態,這個重構會非常不方便。好訊息是,ASP.NET 2.0 增加了對輸出緩衝替換塊(Output Cache Substitution block )的支援,它提供了一個極其乾淨的方法來處理這個情境。
使用 <asp:substitution>控制項的輸出緩衝替換塊:
輸出緩衝替換塊允許你OutputCache整個頁面的輸出,同時在HTML輸出中留下幾個動態地區標記來指明在以後的請求中你需要動態填充內容的地方(譬如,上面例子中的使用者名稱訊息)。我有時把這稱為“甜圈緩衝(donut caching)功能”,因為外部的頁面內容都是緩衝的,只有頁面內容流中間的幾個孔(hole)是動態。這與使用使用者控制項實現的局部頁面緩衝正好相反,因為在局部頁面緩衝的情形下,整個頁面是動態,中間的地區是緩衝的。
你通過使用整頁輸出緩衝的方式來實現輸出緩衝替換,使用與上面 Products.aspx 例子中完全一樣的句法。然後,你可以通過在頁面上添加 <asp:substitution> 控制項來指明頁面的哪些地區你需要動態地使用替換塊來填充,象這樣:
<div class="header">
<h1>Caching Samples</h1>
<div class="loginstatus">
<asp:Substitution ID="Substitution1" runat="server" MethodName="LoginText" />
</div>
</div>
<asp:Substitution> 控制項與ASP.NET中的其他控制項不同,它與 ASP.NET 的輸出緩衝註冊了一個回調事件,當頁面內容在後來的請求中從 ASP.NET輸出緩衝發出時,該事件會導致你的頁面或母板頁的一個靜態方法的調用。這個靜態方法在運行時會傳進一個HttpContext 對象,它包含了ASP.NET Request, Response,User, Server,Session, Application等內在對象,然後你就可以使用它們來返回一個字串,ASP.NET 會在內容發回用戶端前自動把這個字串注入到頁面的相關地區裡去。
譬如,在輸出緩衝的products.aspx 頁面中,為處理上面這個我們需要動態輸出歡迎訊息的情境,我們只要簡單地添加這個方法到我們的Site.Master後台代碼檔案中,然後讓上面這個 <asp:substitution> 控制項來調用:
Partial Class Site
Inherits System.Web.UI.MasterPage
Shared Function LoginText(ByVal Context As HttpContext) As String
Return "Hello " & Context.User.Identity.Name
End Function
End Class
這樣,整個頁面將被輸出緩衝,除了我們頁面右上方的 <asp:substitution> 控制項代表的歡迎訊息的內容外。
很明顯地,我們可以把這個進一步延伸,假如我們想要包括另外象使用者他們的購物籃有多少樣東西這樣個人化的資訊等。非常酷的是,頁面上所有其他的內容仍然是保持完全緩衝的,我們不用在後繼請求裡訪問資料庫來產生其內容,這意味著我們在單獨一個伺服器上每秒鐘就可以處理成千個產品頁。實際上,在請求中,不用產生頁面上的任何控制項,在以後的請求裡,除了上面那個靜態方法外,也沒有編碼會執行,這使得一切都快速無比。
使用Response.WriteSubstitution 方法的輸出緩衝替換塊:
除了使用 <asp:substitution> 控制項在頁面上指定可替換的內容塊外,你也可以使用 Response.WriteSubstitution 方法。這個方法接受一個HttpResponseSubstitutionCallback 方法的delegate對象為參數,你可以在你的應用的任何類裡實現這個方法 (而不限於你後台類裡的靜態方法)。
<asp:substitution> 控制項在內部使用這個方法來接連頁面的後台類裡的delegate方法。同樣地,你也可以在你自己的控制項或頁面使用這個方法,以取得最大的控制和靈活性。
結論:
我還沒有找到一個無法從ASP.NET緩衝功能上受益的ASP.NET應用。因為ASP.NET支援整頁輸出緩衝,局部網頁輸出快取,以及現在的甜圈(donut)層次的緩衝,這允許你根據任何參數或你需要的自訂邏輯來變換緩衝內容,而現在又允許你在資料庫改變時自動使得緩衝內容失效並重建緩衝內容,你不應該發現你自己再會建造一個用不上任何緩衝的應用了。
我絕對建議你花點時間熟悉一下ASP.NET所有的緩衝功能。想找到我完成的有關緩衝的另外的例子的話,請下載我最近在ASP.NET Connections大會上做的技巧/訣竅講座。下載內包括講義和示範代碼,說明如何使用整頁緩衝,局部頁面緩衝,替換塊緩衝(substitution block caching),以及SQL緩衝失效(SQL Cache Invalidation)。
想閱讀我寫的其他ASP.NET技巧/訣竅部落格文章的話,請瀏覽我的ASP.NET技巧,訣竅和資源網頁。
希望本文對你有所協助,
Scott