LANGUAGES: VB.NET | C#
ASP.NET VERSIONS: 1.1+
Make Cache Fast
Gain Scalability and Performance by Mastering the Cache Object
By Steve C. Orr
Small Web applications with few users often don t need to be concerned much with scalability. The Web server and associated database often coexist on the same server, and, therefore, there is relatively little network latency involved on the back end. As a successful Web application grows in popularity, however, things can get more complicated.
As the number of users and associated requests increases, a single server eventually becomes overwhelmed. The database must then be managed by a separate server, leaving more CPU cycles for the Web server to independently handle incoming requests. If the number of incoming requests continues to increase, multiple Web servers may be needed to handle this volume. This is commonly referred to as a Web farm.
While adding more Web servers can be a relatively simple undertaking, adding more database servers is a step that should not be taken lightly, because it could require a major redesign of the application s architecture. Therefore, database resources should be used conservatively. Web servers should make as few calls to the database server(s) as possible.
So what s the best way to reduce the number of database calls?
Reduce Database Requests
Retrieving data from memory is many times faster than retrieving data from disk, especially when that disk is stored on a separate server and network latency is also involved.
With that in mind, ASP.NET introduced the Cache object. The Cache object is used to intelligently store data in Web server memory so fewer database calls are required. During peak conditions, when memory is at a premium, the Cache object will automatically purge less-used data items to free up room for more important ones.
The Cache object can be configured in a variety of ways to customize exactly what gets cached and for how long under varying conditions. Exactly one cache object instance is automatically associated with each ASP.NET Web application, and its scope is application-wide. In other words, the cache and the data contained within it is not associated with any particular page, user, or session.
Any object can be placed into the cache: strings, numbers, custom business objects, and even entire data sets can be stored for blazingly fast access.
Cache Object Best Practices
Placing a data item into the cache could hardly be any easier. Its Insert method requires only a key value and the data object itself, as shown here:
Retrieving data from the cache can be just as easy:
myObject = Cache("Key")
However, because stale items may automatically be removed from the cache, it is recommended that you verify the object was successfully retrieved from the cache. If it wasn t, the data should be manually fetched from the database, then placed in the cache (as shown in Figure 1).
myObject = Cache("Key")
'Check to see if the object was successfully retrieved
If myObject Is Nothing Then
'TODO: Fetch from the database...
Figure 1: Data objects should be fetched from the cache first, then checked to see if the retrieval was successful.
Note that it is possible to check the cache for the existence of the object before retrieving it from the cache. With other kinds of collections, that may indeed be the most efficient approach. However, this technique is not recommended with the Cache object because it could cause rare errors that can be difficult to track down. Figure 2 demonstrates how null reference errors could occasionally be thrown if a data item is automatically removed from the cache at exactly the wrong moment.
If Cache("Key") IsNot Nothing Then
'Item could potentialy be removed from cache here...
myObject = Cache("Key")
'causing the above object to occasionally be null
Figure 2: Counter-intuitively, it is best to retrieve an item from the cache instead of first checking for its existence to avoid null reference errors.
To make the code in Figure 2 work consistently and reliably, you d need to check again to see if the object is null. This redundancy would be inefficient, thus the code in Figure 1 is superior.
The Cache object s Insert method has a variety of optional parameters that act as the gateway to more advanced caching options. For example, the following code specifies that the item being inserted into the cache should be removed in exactly 30 minutes, no matter how frequently the item is being accessed:
myObject, Nothing, _
In contrast to the absolute expiration time specified above, a sliding expiration time can be specified. The code below specifies that the item being inserted into the cache should be removed once it has sat there unused for 30 minutes (if the item is accessed frequently, it might never be removed):
myObject, Nothing, _
New TimeSpan(0, 30, 0))
Absolute expiration cannot be used in conjunction with sliding expiration. They are mutually exclusive. You must choose one parameter or the other, or neither, which is why the NoSlidingExpiration and NoAbsoluteExpiration enumeration values exist.
Another approach for optimizing cache usage is to specify a relative importance to items inserted into the cache. This can be done with the CacheItemPriority Enumeration, which is an optional parameter to the Cache object s Insert method. The following code specifies that the cache item s priority is above normal, making it less likely to be removed from the cache than items added with the normal (default) priority:
myObject, Nothing, _
A cache item s priority can be set to any of these values: Low, BelowNormal, Normal (aka Default), AboveNormal, and High. The final available value is NotRemovable, which specifies that the item will not automatically be removed from the cache when attempts are made to free memory. However, the item may still be removed via other means, such as manually calling the Cache object s Remove method. Any specified absolute or sliding expirations are also still active. For example, the following code dictates that the item being added to the cache will be removed in exactly 30 minutes, and will not automatically be removed if memory becomes scarce:
myObject, Nothing, _
Keeping It Fresh
All the code so far works great for data that doesn t change very often. Lists of countries, states, provinces, postal codes, and other static data can be placed into the cache and forgotten because it almost never changes, and therefore the cache needn t be refreshed at any particular point in time.
But what about more volatile forms of data? Changes to things like user preferences and profile data should ideally be reflected site-wide with little or no delay. The simplest solution would be to avoid storing this kind of dynamic data in the cache, and instead pull it directly from the database. Of course, this can adversely affect scalability. Fortunately, it is possible to have the best of both worlds.
One solution is simply to avoid updating the database and instead directly update only the cache when data changes. This eliminates even more database calls, and thus improves performance. Of course, the database needs to be updated eventually or else the new data will be lost when it is eventually removed from the cache.
Cache Callbacks also can provide a solution. When an item is inserted into the cache, a custom method can be specified, which will be called just before the item is removed from the cache. In this custom method, the database can then be updated with the new data to ensure changes are permanently persisted.
The last (optional) parameter of the Cache object s Add method can be used to specify a custom callback method, as shown here:
sName, Nothing, _
New TimeSpan(0, 1, 0), _
The custom MyCache class referenced above is listed in Figure 3. This custom class physically exists in its own class file in the Web application s App_Code directory.
Public Class MyCache
Public Shared Sub ItemRemovedCallback( _
ByVal key As String, _
ByVal value As Object, _
ByVal removedReason As CacheItemRemovedReason)
If key = "CustomerName" Then
WriteCustomerNameToDB(value) 'Your Custom Function
Figure 3: Cache Callbacks can be used to help ensure underlying data sources get updated.
The value of the removedReason parameter can be examined in cases where it is useful to know why an item was removed from the cache. This enumeration has four possible values. A value of Removed specifies that the item was directly removed from the cache via code, such as calling the cache s Remove method. A value of Expired indicates that the time period associated with this cache item has elapsed. A value of Underused specifies that the item was removed from the cache because memory had to be freed for more important needs. A value of DependencyChanged indicates that the cache item s underlying data source has changed, and therefore this data is now stale.
In cases where Cache Callbacks don t quite meet your requirements, you ll be glad to know there is another potentially useful technique for keeping cached data from getting stale. Items inserted into the cache can be configured to be dependent upon several kinds of underlying data sources. For example, a cache item can be configured to expire when a particular file changes:
Dim depends As New CacheDependency(fileName, DateTime.Now)
Cache.Insert("Key", myObject, depends)
This, in turn, would commonly trigger data to be refreshed from that file.
In addition to file dependencies, cache items also can be dependent upon other cache items. Essentially, a cascading parent-child relationship can be established between cache items, so that if a particular cache item gets removed from the cache, so then will all other cache items that are associated with it. Dependency objects also can be dependent upon other cache dependency objects.
SQL Cache Invalidation
Version 2.0 of ASP.NET introduced a great new feature named SQL Cache Invalidation. When data is returned via ADO.NET, such as a DataSet or DataTable, it can automatically be cached until the underlying data in the database changes. This feature is supported natively by the Broker service in SQL Server 2005 and above.
SQL Server 7.0 and SQL Server 2000 also can support SQL Cache Invalidation, but each individual database and table must be configured to enable the feature. This is done via the command line tool named aspnet_regsql.exe. For these two database versions a section also must be added to the web.config or machine.config file:
<sqlCacheDependency enabled="true" pollTime="2000">
connectionStringName="mydb" pollTime="500" />
This section specifies how often the database and table should be queried to see if the data has changed. In contrast to this persistent polling mechanism, the true push capabilities of SQL Server 2005 and above are clearly more efficient and scalable.
The Cache object is a core tool that every ASP.NET developer is likely to find useful at one point or another. Its automatic memory management is a valuable asset that can be simple to use for simple needs. More demanding requirements also can be fulfilled by the many advanced configuration options exposed by the Cache object s Insert and Add methods.
Web application scalability is a deep topic that can mean many things depending on the number of users and requests involved. ASP.NET provides a variety of tools to help developers deal with scalability and performance issues. While not every possible caching technique can be covered in a single article, the ones discussed here are core concepts with which every professional ASP.NET developer should be familiar.
Steve C. Orr is an ASPInsider, MCSD, Certified ScrumMaster, Microsoft MVP in ASP.NET, and author of Beginning ASP.NET 2.0 AJAX (Wrox). He s been developing software solutions for leading companies in the Seattle area for more than a decade. When he s not busy designing software systems or writing about them, he can often be found loitering at local user groups and habitually lurking in the ASP.NET newsgroup. Find out more about him at http://SteveOrr.net or e-mail him at mailto://[email protected].