導言
就計算機科學而言,caching就是將所需要的數據或信息的備份放在某個地方,便於快速訪問的這樣一個過程。以數據處理(data-driven)程序為例,程序的大部分時間浪費在數據查詢上。要提升這種程序的性能,通常的做法是將查詢結果存放在程序的存儲器裡。
ASP.NET 2.0提供了各種各樣的緩存方式。對web頁面和用戶控件可以通過output caching進行緩存;同樣我們可以通過ObjectDataSource 和SqlDataSource控件,在控件級(control level)對數據進行緩存;同時,ASP.NET的data cache提供了豐富的緩存接口(caching API),供頁面開發員通過編程緩存對象。在本文及接下來的3篇文章我們將對ObjectDataSource的緩存屬性以及data cache進行考察;我們也將探究如何在啟動時對application-wide數據進行緩存,以及通過使用SQL cache dependencies對緩存數據刷新。
主要的緩存要點
由於緩存通過將數據的副本放置在一個便於快速訪問的地方來提高程序的總體性能。由於它僅僅是一個副本,當源數據發生改變時,副本不能同步更新。為此,頁面開發員應制定一個標准將其清除出內存,可以使用如下的2種方法之一:
Time-based標准:向內存添加的條目(item),只能在內存裡駐留固定或靈活(sliding)的一段時間。比如,開發者可設定一個時間段,比如60秒,當條目添加到內存後,不管訪問它的頻率有多高,60秒後就會被清除掉;如果是靈活(sliding)處理的話,當最後一次被訪問後,未再次被訪問的時間一旦超出60秒,也會被清除掉。
Dependency-based標准:當向內存添加條目時為其分配一個從屬體(dependency),當條目對應的從屬體發生改變時將條目清除掉。從屬體可以是一個文件;另一個緩存條目;或者干脆是這兩者的混合體( combination);當然還可以是SQL cache dependencies,它可以向內存添加條目,當源數據改變時將條目清除掉。我們將在接下來的文章《使用SQL緩存依賴項SqlCacheDependency 》裡詳細考察。
不管是哪種標准,在條目被清除掉以前,我們都可以對其訪問。如果內存達到了它的極限,它會清除掉已有的條目後再添加新的條目。因此,當處理緩存數據時很重要的一點是我們要充分考慮到緩存數據已被清除的可能。在下一篇文章《在分層架構中緩存數據》我們考察采用哪種模式從內存訪問數據。
緩存是提升程序性能的一種較為經濟的方法,就像Steven Smith在他的文章《ASP.NET Caching: Techniques and Best Practices:》裡闡述的一樣:“緩存是獲得‘上佳'性能的一種好方法,不需要太多的時間和分析。… 存儲器也便宜,要獲得你期望的性能,靠緩存技術你需要花30秒;靠優化代碼和數據庫你可能要幾天乃至幾周時間…”
雖然緩存可以顯而易見的提升系統性能,但並不是適用於所有的應用程序,比如某些實時(real-time)、頻繁更新數據的程序就不適合。
但是對大部分程序而言,還是適用的。關於ASP.NET 2.0裡的緩存的更多背景資料請參考ASP.NET 2.0 QuickStart Tutorials系列的Caching for Performance 部分。
第一步:創建Caching頁面
在我們開始以前,首先讓我們花些時間來添加包括本篇在內的最近四篇教程需要用到的頁面。我們先在項目中新建一個稱作Caching的文件夾,接下來,為目錄新增以下幾個頁面,並配置為使用Site.master母板頁。
Default.aspx
ObjectDataSource.aspx
FromTheArchitecture.aspx
AtApplicationStartup.aspx
SqlCacheDependencies.aspx
圖1:創建相關的ASP.NET頁面
像其它文件夾一樣,Caching文件夾裡的Default.aspx頁面將本系列的文章顯示出來。記得用戶控件SectionLevelTutorialListing.ascx提供該功能,設計模式裡將其拖到頁面上。
圖2:為Default.aspx頁面添加用戶控件SectionLevelTutorialListing.ascx
最後,將這些頁面添加到Web.sitemap文件裡,特別的,放在“Working with Binary Data” <siteMapNode>:之後:
<siteMapNode title="Caching" url="~/Caching/Default.aspx" description="Learn how to use the caching features of ASP.NET 2.0."> <siteMapNode url="~/Caching/ObjectDataSource.aspx" title="ObjectDataSource Caching" description="Explore how to cache data directly from the ObjectDataSource control." /> <siteMapNode url="~/Caching/FromTheArchitecture.aspx" title="Caching in the Architecture" description="See how to cache data from within the architecture." /> <siteMapNode url="~/Caching/AtApplicationStartup.aspx" title="Caching Data at Application Startup" description="Learn how to cache expensive or infrequently-changing queries at the start of the application." /> <siteMapNode url="~/Caching/SqlCacheDependencies.aspx" title="Using SQL Cache Dependencies" description="Examine how to have data automatically expire from the cache when its underlying database data is modified." /> </siteMapNode>
完成Web.sitemap文件的更新後,讓我們在浏覽器裡查看,左邊的菜單欄顯示caching章節的文章
圖3:網站地圖Site Map包含了Caching章節的文章
第二步:在Web Page頁面裡展示產品
本文考察怎樣使用ObjectDataSource控件內置(built-in)的緩存功能。在開始之前,我們首先需要創建一個頁面,用一個ObjectDataSource控件調用ProductsBLL class類獲取產品信息,再用GridView控件展示出來。
首先打開Caching文件夾裡的ObjectDataSource.aspx頁面。從工具箱拖一個GridView控件到頁面,設置其ID為Products,再從智能標簽裡選擇將其綁定到一個ObjectDataSource控件,ID為ProductsDataSource。設該ObjectDataSource使用ProductsBLL class類。
圖4:設置ObjectDataSource控件使用ProductsBLL Class類
在本頁面,我們要創建一個允許編輯的GridView控件,當ObjectDataSource控件裡的緩存數據發生改變時,我們可以通過GridView的界面查看到底會發生什麼。在SELECT標簽裡選擇默認的GetProducts()方法, 但是在UPDATE標簽裡選擇接受productName, unitPrice 和productID作為輸入參數的UpdateProduct()重載方法。
圖5:在UPDATE標簽裡選擇重載的UpdateProduct()方法
最後,在INSERT和DELETE標簽裡選擇“(None)”,點完成按鈕。一旦完成“設置數據源向導”,Visual Studio會將ObjectDataSource控件的OldValuesParameterFormatString屬性設置為original_{0}。就像在前面的教程之16章《概述插入、更新和刪除數據》裡探討的一樣,該屬性要麼刪除掉,要麼設置為{0},不然的話更新操作會報錯。
此外,完成向導後,Visual Studio會將產品的所有數據列添加到GridView控件,將除了ProductName, CategoryName和UnitPrice之外的所有綁定列(BoundFields)刪除。然後,分別將上述3列的HeaderText屬性改為Product”, “Category”和“Price”。由於ProductName是必需的,將ProductName列轉變成模板列(TemplateField),在EditItemTemplate裡添加一個RequiredFieldValidator控件;同樣的,將UnitPrice列也轉換成模板列,並添加一個CompareValidator控件,確保用戶輸入的是大於或等於0的有效的貨幣值。除此以外,你還可以作一些界面上的改進,比如使UnitPrice值居中,或分別對UnitPrice的只讀和編輯界面作一些格式化的處理。
在GridView的智能標簽裡點相關項啟動編輯、分頁、排序功能。
注意:想回顧怎樣自定義GridView的編輯界面嗎?請參考前面的文章之20《定制數據修改界面》
圖6:啟用GridView的編輯、排序、分頁功能。
完成GridView的修改後,GridView 和 ObjectDataSource的代碼聲明看起來像下面這樣:
<asp:GridView ID="Products" runat="server" AutoGenerateColumns="False" DataKeyNames="ProductID" DataSourceID="ProductsDataSource" AllowPaging="True" AllowSorting="True"> <Columns> <asp:CommandField ShowEditButton="True" /> <asp:TemplateField HeaderText="Product" SortExpression="ProductName"> <EditItemTemplate> <asp:TextBox ID="ProductName" runat="server" Text='<%# Bind("ProductName") %>'></asp:TextBox> <asp:RequiredFieldValidator ID="RequiredFieldValidator1" Display="Dynamic" ControlToValidate="ProductName" SetFocusOnError="True" ErrorMessage="You must provide a name for the product." runat="server">*</asp:RequiredFieldValidator> </EditItemTemplate> <ItemTemplate> <asp:Label ID="Label2" runat="server" Text='<%# Bind("ProductName") %>'></asp:Label> </ItemTemplate> </asp:TemplateField> <asp:BoundField DataField="CategoryName" HeaderText="Category" ReadOnly="True" SortExpression="CategoryName" /> <asp:TemplateField HeaderText="Price" SortExpression="UnitPrice"> <EditItemTemplate> $<asp:TextBox ID="UnitPrice" runat="server" Columns="8" Text='<%# Bind("UnitPrice", "{0:N2}") %>'></asp:TextBox> <asp:CompareValidator ID="CompareValidator1" ControlToValidate="UnitPrice" Display="Dynamic" ErrorMessage="You must enter a valid currency value with no currency symbols. Also, the value must be greater than or equal to zero." Operator="GreaterThanEqual" SetFocusOnError="True" Type="Currency" runat="server" ValueToCompare="0">*</asp:CompareValidator> </EditItemTemplate> <ItemStyle HorizontalAlign="Right" /> <ItemTemplate> <asp:Label ID="Label1" runat="server" Text='<%# Bind("UnitPrice", "{0:c}") %>' /> </ItemTemplate> </asp:TemplateField> </Columns> </asp:GridView> <asp:ObjectDataSource ID="ProductsDataSource" runat="server" OldValuesParameterFormatString="{0}" SelectMethod="GetProducts" TypeName="ProductsBLL" UpdateMethod="UpdateProduct"> <UpdateParameters> <asp:Parameter Name="productName" Type="String" /> <asp:Parameter Name="unitPrice" Type="Decimal" /> <asp:Parameter Name="productID" Type="Int32" /> </UpdateParameters> </asp:ObjectDataSource>
如圖7所示,GridView列出了每個產品的name, category和price信息。花幾分鐘測試頁面—對結果排序,查看分頁,編輯某條記錄。
圖7:顯示每條記錄的Name, Category和Price信息
第三步:考察ObjectDataSource如何請求數據
ID為Products的GridView通過調用名為ProductsDataSource的ObjectDataSource的Select()方法檢索數據並將它顯示出來。該ObjectDataSource創建業務邏輯層的ProductsBLL class類的一個實例並調用它的GetProducts()方法,該方法又調用數據訪問層ProductsTableAdapter的GetProducts()方法。數據訪問層連接到數據庫Northwind,並執行已設置好了的SELECT查詢。查詢數據以NorthwindDataTable的形式返回到數據訪問層,該DataTable對象再依次傳回到業務邏輯層,ObjectDataSource、GridView控件。GridView控件為DataTable裡的每一數據行(DataRow)創建一個GridViewRow對象,每個GridViewRow對象最終被編譯為HTML返回到客戶端,呈現在訪問者的浏覽器裡。
任何時候,當GridView控件需要綁定時,按上述的事件發生順序執行。比如,首次登錄頁面;將數據從一個頁面傳遞到另一個頁面;在GridView裡排序;通過GridView內建的編輯或刪除界面改動數據。當GridView的視圖(view sta被設為disabled時,每次頁面回傳時也會對GridView重新綁定;當然我們可以顯式地調用DataBind()方法來對GridView實施綁定。
為了更清除地揭示從數據庫檢索數據的頻率,我們顯示一個消息,提示在某時程序在檢索數據。為此,在GridView控件上添加一個ID為ODSEvents的Label控件,清除其Text屬性,將其EnableViewState屬性設置為false。在Label控件下面再添加一個Button控件,設其Text屬性為“Postback”.
圖8:在GridView上添加Label 和 Button控件
在整個數據檢索過程中,首先觸發ObjectDataSource的Selecting事件,並調用其對應的已設置好的方法。為該事件創建一個事件處理器,添加如下的代碼:
protected void ProductsDataSource_Selecting(object sender, ObjectDataSourceSelectingEventArgs e) { ODSEvents.Text = "-- Selecting event fired"; }
每當ObjectDataSource開始檢索數據時,Label控件都會顯示文本“Selecting event fired”.
在浏覽器訪問該頁面。當首次登錄時,文本“Selecting event fired”就會顯示出來。點“Postback”按鈕時,我們注意到文本消失了(前提是你將GridView的EnableViewState屬性設置為默認值true)。這是因為當頁面回傳時,GridView通過它的視圖狀態(view state)載入數據進行重建(reconstructed),因此不再需要通過ObjectDataSource檢索數據庫來得到數據進行重建。然而,排序、分頁、編輯等都會促使GridView重新綁定到數據源,因此,文本“Selecting event fired”又出現了。
圖9:當GridView重新綁定到數據源時,顯示文本“Selecting event fired”
圖10:點“Postback” 按鈕導致GridView從視圖狀態“View State”獲取數據
每次分頁、排序時都需要從數據庫檢索數據,這看起來有點浪費資源。即便GridView不支持排序和分頁,任何人每次第一次登錄頁面時都需要從數據庫檢索數據(如果將view state設置為disabled的話,每次頁面回轉也會檢索數據)。如果GridView對所有用戶顯示的數據都是一樣話,那麼額外的數據庫查詢是浪費。我們可以對GetProducts()方法返回的數據進行緩存,再將GridView綁定到這些緩存數據。
第四步:用ObjectDataSource緩存數據
僅僅簡單的設置某些屬性,我們就可以讓ObjectDataSource對它的檢索數據自動的進行緩存。以下總結了ObjectDataSource控件的與緩存相關的屬性:
EnableCaching—必須設置為true,默認為false.
CacheDuration—緩存時間,以秒為單位。默認為0,只有當EnableCaching屬性設置為true,且CacheDuration設為大於0的值時ObjectDataSource控件才會緩存數據。
CacheExpirationPolicy—可設置為Absolute 或 Sliding。如果為Absolute,當它設為多少秒時,ObjectDataSource就會對檢索的數據緩存多少秒;如果為Sliding,當它設為多少秒時,一旦超過那麼多秒沒有對緩存數據進行訪問,就終止緩存。默認為Absolute。
CacheKeyDependency—用該屬性將ObjectDataSource的緩存條目(entry)與現有的緩存從屬體關聯起來。利用可以它將緩存條目提前從內存清除掉。絕大多數情況下用該屬性把SQL cache dependency與ObjectDataSource的緩存關聯起來。這個話題我們將在後面的教程《使用SQL緩存依賴項SqlCacheDependency 》考察。
讓我們設置ID為ProductsDataSource的ObjectDataSource 的數據緩存時間為30秒。設其EnableCaching屬性為true;設其CacheDuration屬性為30;CacheExpirationPolicy屬性為默認的Absolute。
圖11:設置ObjectDataSource的緩存時間為30秒
保存你的設置,並在浏覽器裡查看。當你第一次登錄頁面時,文本“Selecting event fired”會顯示出來,因為原始數據還未緩存。但你點“Postback”按鈕,或進行分頁,排序,或點編輯、取消按鈕時,文本“Selecting event fired”就不會顯示出來了。原因是只有當ObjectDataSource控件檢索數據時才會觸發Selecting事件;如果ObjectDataSource控件是從緩存裡面獲取數據的話就不會觸發Selecting事件。
過了30秒後,數據將從內存清除;或者調用ObjectDataSource控件的Insert, Update,或Delete方法的話數據也會被清除掉。因此,過了30秒後或點擊“Update”按鈕,編輯,取消按鈕,或排序、分頁的話就會促使ObjectDataSource檢索數據,觸發Selecting事件,文本“Selecting event fired”又會顯示出來。最後,再對檢索得到的數據進行緩存。
注意:如果你看到文本“Selecting event fired”頻繁的出現,很可能是內存容量太小。如果沒有足夠的容量,ObjectDataSource添加到內存的數據可能被清除掉了。如果ObjectDataSource沒有或者只是偶爾地對數據緩存,請關閉一些應用程序來釋放掉內存,然後再試一次。
圖12揭示了ObjectDataSource的緩存流程。當文本“Selecting event fired”出現在屏幕上時,那是因為數據沒有在緩存裡找到,必須進行相關檢索。當文本消失時,那是因為數據進行了緩存。當從緩存得到了所需的數據時,沒有任何數據查詢執行。
圖12:ObjectDataSource在Data Cache裡存儲和獲取數據
每一個ASP.NET應用程序有它自己的數據緩存實例,所有的頁面和用戶都可以進行訪問。那意味著對於ObjectDataSource控件緩存的數據,所有登錄該頁面的用戶都可以訪問。來做個驗證,在一個浏覽器裡打開ObjectDataSource.aspx頁面,當第一次登錄該頁面時,文本“Selecting event fired”顯現出來(假定前面測試時緩存的數據到此時已經被清除掉了)。再開第二個浏覽器,將第一個浏覽器裡的URL地址拷貝、粘貼過來。在第二個浏覽器裡,文本“Selecting event fired”並沒有顯示出來,因為它使用的是第一個浏覽器頁面緩存的數據。
當向內存添加檢索數據時,ObjectDataSource要用到一個叫cache key的值,該值包括:CacheDuration 和 CacheExpirationPolicy屬性的值;ObjectDataSource調用的業務對象的類型(type),它由TypeName 屬性指定(比如:ProductsBLL);SelectMethod 屬性的值,以及SelectParameters參數集裡參數的name 和 values;StartRowIndex 和 MaximumRows屬性的值,它用來執行用戶自定義分頁(custom paging)。
將這些屬性值組合在一起構成cache key值是為了對每一個緩存條目提供唯一的標識值。比如,在前面的教程裡,我們使用ProductsBLL類的GetProductsByCategoryID(categoryID)方法來獲取某個指定類的所有產品。假如一個用戶在頁面查看飲料類(其CategoryID值為1)的產品信息,如果ObjectDataSource控件在進行數據緩存時忽略SelectParameters的值,當另一個用戶登錄頁面查看調味品類的產品信息時,恰好飲料類產品信息正好緩存在內存裡,第二個用戶將會看到飲料類的產品信息,而非他想要的調味品類的產品信息。所以,當cache key值包含electParameters的值的話,ObjectDataSource緩存數據的時候就可以將調味品類和飲料類區分開來。
數據更新不同步(Stale Data)問題
當調用ObjectDataSource控件的Insert, Update和 Delete其中一個方法時,它都會將緩存條目從內存清除掉。這樣做的好處在於當從頁面修改數據時將緩存的舊數據清除掉。然而,ObjectDataSource還是可能有將“未更新數據”(也就是源數據已經發生改變,而緩存的數據沒同步更新)顯示出來的情況。最簡單的例子是直接從數據庫修改數據,比如某個數據庫管理員運行一個腳本,修改數據庫裡的某些記錄。
在此,我們探討一種微妙的情況。雖然調用ObjectDataSource的數據修改方法時它會將緩存數據清除掉,但清除的是那些與ObjectDataSource的“屬性組合值”(combination of property)相匹配的緩存條目(比如CacheDuration, TypeName, SelectMethod等)。假如你有2個ObjectDataSources控件,它們更新相同的數據,當使用不同的SelectMethods 或 SelectParameters,當第一個ObjectDataSources控件更新某一行記錄而清除該行對應的緩存數據時,第二個ObjectDataSources控件仍然使用該行對應的緩存數據。我們來做個驗證,創建一個頁面,包含可編輯的GridView控件,其對應的ObjectDataSource控件設置為使用緩存,且調用ProductsBLL類的GetProducts()方法來獲取數據。在本頁(或另外創建一個頁面)再添加GridView 和ObjectDataSource控件,但是設置第二個ObjectDataSource控件調用GetProductsByCategoryID(categoryID)方法。由於這2個ObjectDataSource控件的SelectMethod屬性不同,因此它們各自有各自不同的緩存值。如果你在第一個GridView控件裡編輯某個產品,然後在第二個GridView控件裡重新綁定數據(比如分頁、排序等),我們在第二個GridView控件裡看到該產品的值依然是“老的緩存數據”(而並不是第一個GridView控件修改後的值)
簡而言之,如果你樂於使用“老的緩存數據”,那只有使用基於時間的緩存時間值(time-based expiries,也就是設置具體的緩存時間長度),如果對數據及時更新要求很高的話,將緩存時間設短點。如果不允許使用“老的緩存數據”的話,要麼放棄緩存,要麼使用SQL cache dependencies(你可以認為它是你緩存的數據庫數據)。我們將在後面探討SQL cache dependencies。
總結:
在本文我們考察了ObjectDataSource內建的緩存功能。僅僅設置很少的屬性,我們可以將ObjectDataSource調用SelectMethod方法得到的數據進行緩存。其CacheDuration 和 CacheExpirationPolicy屬性指定了緩存的時間和類型(absolute或sliding)。而CacheKeyDependency屬性將ObjectDataSource的緩存實體與現有的緩存從屬體(cache dependency)關聯起來,一般是SQL cache dependencies。
因為ObjectDataSource只是簡單地進行數據緩存,我們可以通過編程實現ObjectDataSource內建的這種功能。不過在表現層這樣做沒有意義,因為ObjectDataSource控件提供了該功能。不過我們可以在體系結構的其它層次實現緩存功能。為此,我們需要一個邏輯,它與ObjectDataSource調用的相同。在下一篇文章我們將考察如何在體系結構內部編程處理數據緩存。
祝編程快樂!
作者簡介
本系列教程作者 Scott Mitchell,著有六本ASP/ASP.NET方面的書,是4GuysFromRolla.com的創始人,自1998年以來一直應用 微軟Web技術。大家可以點擊查看全部教程《[翻譯]Scott Mitchell 的ASP.NET 2.0數據教程》,希望對大家的學習ASP.NET有所幫助。