9.3 Data Caching


Internally, the output cache is built using a sophisticated data caching engine. This data caching engine is available directly to page developers as well through the Cache property of the Page class and should be used in addition to output caching (or instead of it, in some cases) to improve response times.

Caching of data can dramatically improve the performance of an application by reducing database contention and round-trips. The data cache provided by ASP.NET gives you complete control over how data that you place in the cache is handled. At its simplest level, data caching can be used as a way to store and restore values in your application, which is trivial to do using its dictionary interface. The example shown in Listing 9-14 demonstrates the caching of a DataView that has been populated from a database query. The first time this page is accessed, the database is queried, the DataView is populated, and it is then placed in the cache. On subsequent accesses , the DataView will be retrieved from the cache, saving the time required to query the database again.

Listing 9-14 Caching a DataView in the Data Cache
 <! File: DataViewCache.aspx > <%@ Page Language="C#" %> <%@ Import Namespace="System.Data" %> <%@ Import Namespace="System.Data.SqlClient" %> <html> <script runat="server"> protected void Page_Load(Object src, EventArgs e) {   // Look in the data cache first   DataView dv = (DataView)Cache["EmployeesDataView"];   if (dv == null)  // wasn't there   {     SqlConnection conn = new SqlConnection("server=localhost;uid=sa;pwd=;database=Test");     SqlDataAdapter da =        new SqlDataAdapter("select * from Employees", conn);     DataSet ds = new DataSet();     da.Fill(ds, "Employees");     dv = ds.Tables["Employees"].DefaultView;     dv.AllowEdit   = false;     dv.AllowDelete = false;     dv.AllowNew    = false;       // Save employees table in cache     Cache["EmployeesDataView"] = dv;     conn.Close();   }   else     Response.Write("<h2>Loaded from data cache!</h2>");   lb1.DataSource = dv;   lb1.DataTextField = "Name";   lb1.DataValueField = "Age";   DataBind(); } </script> <body> <form runat="server"> <asp:ListBox id="lb1" runat=server /> </form> </body> </html> 

The data cache exists at the scope of the application and in many ways is identical in functionality to the application state bag ( HttpApplicationState ), with two important differences. First, anything placed in the data cache is not guaranteed to be there when you attempt to retrieve it again (by default). This means that you should always be prepared for a cache miss by being able to retrieve the data from its original source if the cache returns an empty value, as demonstrated in the previous example. The second difference is that the data cache is not intended as a place to store shared, updateable data. Because the cache lives at the application scope, the potential for concurrent access is high, and in fact, the Cache class uses a multireader, single-writer synchronization object ( System.Threading.ReaderWriterLock ) to ensure that no more than one thread modifies the cache at a time. This synchronization object, however, is not exposed externally and thus cannot be used by clients to perform their own locking. This is in contrast to the HttpApplicationState class, which provides a pair of methods , Lock() and an UnLock() to have clients perform explicit locking whenever modifications are made to the application state. It is also important to keep in mind that the cache lives at the application scope in a particular instance of the ASP.NET worker process and is not shared between processes or machines. This means that cached data is not intrinsically synchronized across machines in a Web farm.

As a result, the proper and intended use of the data cache is to store read-only data or objects for the convenience of access. Note that in the previous example, the DataView that was cached was modified to prevent updates, deletes, or insertions, effectively making it read-only. It is good practice to make cache entries read-only to ensure that cached data is not accidentally modified. The example in Listing 9-15 shows how not to use the data cache.

Listing 9-15 Improper Use of the Data Cache
 <! File: BadCache.aspx > <%@ Import Namespace="System.Collections" %> <html> <script language="C#" runat="server"> protected void Page_Load(Object src, EventArgs e) {   // Look in the data cache first   ArrayList al = (ArrayList)Cache["MyList"];   if (al == null)  // wasn't there   {      al = new ArrayList();       // Save ArrayList in cache     Cache["MyList"] = al;   }   // Manipulate the ArrayList by adding the time this   // request was made (bad! may be accessed concurrently!)   al.Add(DateTime.Now.ToString());   lb1.DataSource = al;   DataBind(); } </script> <body> <form runat="server"> <asp:ListBox id="lb1" runat=server /> </form> </body> </html> 

In this example, an instance of the ArrayList class is stored in the cache. It is modified every time the page is hit by adding the time of the current request. This is dangerous because multiple client requests may come in concurrently to this application, and the ArrayList class is not thread-safe by default.

The data cache is also used internally to manage the HTTP pipeline. It is often instructive to view the contents of this data cache, including all system-cached objects and any you may have added to the cache. You easily can do this by calling the function shown in Listing 9-16 from within any ASP.NET page.

Listing 9-16 Displaying the Contents of the Data Cache
 private void PrintDataCache() {   string strCacheContents;   string strName;   //display all of the items stored in the ASP.NET cache   Response.Write("<b>Data cache contains:</b><br/>");   Response.Write("<table>");   Response.Write("<tr><td><b>Key</b></td>");   Response.Write("<td><b>Value</b></td></tr>");   foreach(object objItem in Cache)   {     Response.Write("<tr><td>");     DictionaryEntry de = (DictionaryEntry)objItem;     Response.Write(de.Key.ToString());     Response.Write("</td><td>");     Response.Write(de.Value.ToString());     Response.Write("</td></tr>");   }   Response.Write("</table>"); } 

9.3.1 Cache Entry Attributes

So far we have seen that the data cache is similar to the application state object except for object lifetime and updateability. There are several other differences as well, primarily related to determining the lifetime of an object in the cache. Each time a new item is inserted into the cache, it is added with a collection of attributes. Every cache entry is represented by an instance of the private CacheEntry class, which is created on behalf of your item when you perform a cache insertion. While you don't have direct access to this class when using the cache, you can control the attributes of each instance when you add objects to the cache. Table 9-4 shows the various properties of the CacheEntry class and their meanings.

Table 9-4. CacheEntry Properties

Property

Type

Description

Key

String

A unique key used to identify this entry in the cache

Dependency

CacheDependency

A dependency this cache entry has ”either on a file, a directory, or another cache entry ”that, when changed, should cause this entry to be flushed

Expires

DateTime

A fixed date and time after which this cache entry should be flushed

Sliding Expiration

TimeSpan

The time between when the object was last accessed and when the object should be flushed from the cache

Priority

CacheItemPriority

How important this item is to keep in the cache compared with other cache entries (used when deciding how to remove cache objects during scavenging)

OnRemoveCallback

CacheItem RemovedCallback

A delegate that can be registered with a cache entry for invocation upon removal

When the default indexer of the data cache is used to insert items, as was shown in the previous examples, the values of the CacheEntry class are set to default values. This means that the expiration is set to infinite, the sliding expiration is at 0, the CacheItemPriority is Normal , and the CacheItemRemoveCallback is null . Basically, your object will remain in the cache as long as no scavenging operation occurs (typically because of excessive process memory usage) and you don't explicitly remove it.

If you want more control over the attributes of the CacheEntry created for your cached object, you can use one of several overloaded versions of the Insert() method. The most verbose version of Insert() takes all the CacheEntry properties as parameters (plus the object to be cached) and passes them into the constructor for the CacheEntry class. For example, the code shown in Listing 9-17 inserts a string into the data cache that is set to expire a second before midnight on December 31, 2001.

Listing 9-17 Setting Expiration Dates in the Data Cache
 object obj = // retrieve obj to place in cache somehow DateTime dt = new DateTime(2001, 12, 31, 23, 59, 59); Cache.Insert("MyVal", // key              obj,     // object              null,    // dependencies              dt,      // absolute expiration              Cache.NoSlidingExpiration, // sliding exp.              CacheItemPriority.Default, // priority              null);   // callback delegate 
9.3.1.1 Cache Object Lifetime

Whenever data is added to the data cache, you must specify its lifetime (or implicitly accept the default lifetime of infinite ). This is an important decision because it directly affects the correctness of data retrieval in your application, and if not done correctly, can lead to working with stale data, often referred to as cache coherency problems. How you determine the lifetime of the data that you place in the cache depends entirely on the type of data you are caching. The data may become invalid when a file changes on the system or when another cache entry becomes invalid. It may become invalid after a fixed period of time (absolute expiration). Or perhaps the data is not in danger of becoming stale, but you don't want it to occupy memory in the cache unless it is actually being referenced (achieved with sliding expiration times). Finally, you can register a callback delegate for the data cache to invoke whenever a particular item is removed from the cache if you want to take specific action when the item is removed.

All these options can be specified when you insert an item into the cache using the Cache.Insert() method. The code in Listing 9-18 shows an example of adding the contents of a file to the data cache on application start (in the global.asax file). This cache entry becomes invalid if the contents of the file used to populate the cache entry change, so a CacheDependency is added to the file. We also register a callback function to receive notification of when the data is removed from the cache. Finally, this entry is set to have no absolute expiration, no sliding expiration, and the default value for priority.

Listing 9-18 Using Cache Dependencies
 <! File: global.asax > <%@ Application Language="C#" %> <script runat=server> public void OnRemovePi(string key, object val,                        CacheItemRemovedReason r) {      // Perhaps perform some action in response to      // cache removal here } public void Application_OnStart() {   System.IO.StreamReader sr =      new System.IO.StreamReader(Server.MapPath("pi.txt"));   string pi = sr.ReadToEnd();   CacheDependency piDep =      new CacheDependency(Server.MapPath("pi.txt"));   Context.Cache.Add("pi", pi, piDep,                     Cache.NoAbsoluteExpiration,                     Cache.NoSlidingExpiration,                     CacheItemPriority.Default,           new CacheItemRemovedCallback(OnRemovePi)); } </script> 

Any page that was part of this application could then reference the "pi" key in the data cache and be guaranteed that it is always up to date with the contents of the pi.txt file. Listing 9-19 shows how it might be used ”in this case, to populate the contents of a text box with the value of the string in the file.

Listing 9-19 Sample Page Accessing a Cache Element
 <! File: PiPage.aspx > <%@ Page language=C# %> <html> <head> <script runat=server> protected void Page_Load(Object src, EventArgs e) {   if (Cache["pi"] == null)   {      // Refresh pi in app      pi.Text =        ((global_asax)Context.ApplicationInstance).LoadPi();   }   else     pi.Text = (string)Cache["pi"]; } </script> </head> <body> <form runat="server"> <h1>The pi Page</h1> <asp:TextBox id="pi" runat=server Rows=50 Wrap=True              Width=450px TextMode=MultiLine              Height=300px/> </form> </body> </html> 

9.3.2 Cache Object Removal

An object in the data cache can be removed in several ways. You can explicitly remove it from the cache using the Cache.Remove method, it can be removed because its lifetime has expired , or it can be implicitly removed from the cache to reduce memory consumption (scavenging). You have direct control over the first two cases. You explicitly call Cache.Remove , and you explicitly set the expiration date of items in the cache. Removal because of scavenging, however, is not always under your direct control. You can indicate a preference for how your cache items should be treated during a scavenging operation, however.

When scavenging is performed, the data cache removes items with low priority first. By default, your cache items have normal priority. If you want to directly control the priority of your cache items, you can set the priority value when you perform the insertion into the cache. Table 9-5 shows the various values for CacheItemPriority and their meanings. Note that you can request that an item in the cache not be removed during scavenging. Most of the time, it is wise to leave these priority values at their defaults and let the cache use its scavenging algorithms to decide which objects to remove.

Table 9-5. CacheItemPriority Values

CacheItemPriority Value

Description

AboveNormal

Item less likely than Normal items to be removed from cache during scavenging

BelowNormal

Item more likely than Normal items to be removed from cache during scavenging

Default

Equivalent to Normal

High

Least likely to be deleted from the cache during scavenging

Low

Most likely to be deleted from the cache during scavenging

Normal

Deleted from the cache after all Low and BelowNormal items have been deleted during scavenging

NotRemovable

Never removed from the cache implicitly

9.3.3 Data Cache Considerations and Guidelines

As with the output cache, using the data cache effectively involves making important decisions about data lifetime and estimating trade-offs in memory consumption and throughput. The following guidelines and considerations are designed to help you use the data cache as efficiently as possible.

  1. The data cache is not a container for shared updateable state.

    You should always anticipate the possibility that a request for an item in the data cache will return null , and you should never modify existing items (although replacing them with new objects is fine). The data cache does protect against concurrent writes to the Hashtable that is used internally to store cache entries, but that concurrency protection does not extend to accessing and modifying objects in the cache. In general, it is a bad idea to use any shared updateable state at the application scope anyway, because it often can become a bottleneck in application performance.

  2. Cache data that is accessed frequently and is relatively expensive to acquire.

    The effectiveness of caching data in a Web application depends on two factors: how often the data is accessed and how often it changes. As with output caching, if the data changes with each client request, caching it is a complete waste of resources. On the other hand, if the data does not change frequently but is almost never accessed, it is also a waste of resources to cache it (especially if it is big). Caching data retrieved from a database, especially if the database is on a remote machine, is almost always beneficial.

  3. If data is dependent on a file, directory, or other cache entry, use a CacheDependency to be sure it remains current.

    Many cache entries may have dependencies on external resources, or perhaps other cache entries. It is easy to ensure that these entries stay current by adding a CacheDependency when inserting them into the cache. Note that you can also signal that a cache entry is out of sync with a database value by adding a trigger to the database that modifies a file whenever the data is changed.

  4. Beware cache coherency problems.

    With the dramatic performance improvement that data caching brings , it is easy to begin relying on it too much and introducing cache coherency problems into your system. When you add data to the cache, carefully think through the different ways in which it will be accessed, and make sure that the data is not stale or that stale data is acceptable. In most cases, you can still achieve significant performance improvements even with short cache lifetime durations, especially if the data is accessed frequently.



Essential ASP.NET With Examples in C#
Essential ASP.NET With Examples in C#
ISBN: 0201760401
EAN: 2147483647
Year: 2003
Pages: 94
Authors: Fritz Onion

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net