6. SQL Server Cache Dependency
Many ASP.NET applications query some data out of
a database, cache it, and then manage to serve a report to the user.
Binding the report to the data in the cache will both reduce the time
required to load each report and minimize traffic to and from the
database. What’s the problem, then? With a report built from the cache,
if the data displayed is modified in the database, users will get an
out-of-date report. If updates occur at a known or predictable rate, you
can set an appropriate duration for the cached data so that the report
gets automatically refreshed at regular intervals. However, this
contrivance just doesn’t work if serving live data is critical for the
application or if changes occur rarely and, worse yet, randomly. In the
latter case, whatever duration you set might hit the application in one
way or the other. A too-long duration creates the risk of serving
outdated reports to end users which, in some cases, could undermine the
business; a too-short duration burdens the application with unnecessary
queries.
A database dependency is a special case of
custom dependency that consists of the automatic invalidation of some
cached data when the contents of the source database table changes. Not
directly supported by the framework in ASP.NET 1.x, database
dependencies are a native feature starting with ASP.NET 2.0. In ASP.NET
2.0 and beyond, you find an ad hoc class—SqlCacheDependency—that inherits CacheDependency
and supports dependencies on SQL Server tables. More precisely, the
class is compatible with MSDE, SQL Server 7.0, and subsequent SQL Server
versions (including SQL Server 2005).
Under the Hood of Database Dependencies
With the notable exception of SQL Server 2005,
no database today offers listening features to detect when relevant
changes occur. This means that on SQL Server 7.0, SQL Server 2000, and
non-SQL Server databases you must create a database-level infrastructure
that makes table changes emerge, allows them to be captured, and
notifies the ASP.NET cache of the changes.
In the past several years, a few techniques
have been investigated by the ASP.NET team and developers in the
community. None of the techniques is perfect, but all are worth a look
if you plan to implement database cache invalidation in ASP.NET 1.x
applications.
A database cache invalidation system is based
on two key elements—a mechanism to detect changes on the database, and a
mechanism to notify ASP.NET of the changes. To detect changes, triggers
are the most commonly used technique. You need to have a trigger active
on each table in a database that you want to monitor for changes. The
trigger captures insertions, deletions, and updates on a table and does
something with them. What exactly it does depends on the second
mechanism you implement. Various approaches have been tested over time:
An extended stored procedure invokes an
application-specific HTTP handler (which is simpler than a plain page).
The handler receives the key of the cache item that has been
invalidated and removes it from the cache.
An extended stored procedure modifies the timestamp of a disk file that the cached data is dependent upon.
Although some people
don’t particularly like the use of triggers, as I see things the real
issue here is the use of extended stored procedures. They have to be
written in C++ and deployed manually to SQL Server. Furthermore, they
require administrative permissions because they run external programs
and introduce potentially serious blocking issues. The extended stored
procedure can’t return until the HTTP call or the file modification is
complete. What if the Web server takes a long time to respond? What if
the file is locked? In the end, the database will be affected and the
flow of information from it or to it might be slowed down. These
solutions might work great for small applications with no scalability
concerns, but they are not ideal for large, real-world sites.
Tip
If you don’t like
triggers, you might want to try T-SQL checksum functions. The following
query returns a value that varies when changes are made to a table
record: SELECT CHECKSUM_AGG(BINARY_CHECKSUM(*)) FROM Customers
Checksum functions are reasonably
fast but don’t work with reference columns such as text and image. The
advantage of checksum functions is that all you need to deploy on the
database is a stored procedure to wrap the query just shown. |
Extended stored procedures implement a push
model, where the database backend pushes changes to the ASP.NET
application. The reverse approach is also possible—a pull model based on
polling—which is the foundation of the ASP.NET implementation of
database cache invalidation.
The database to be monitored is equipped with
triggers and a helper table with one record for each monitored table.
Triggers update the helper table whenever the corresponding table is
modified. A custom component placed in the ASP.NET cache polls this
helper table looking for changes—and because it’s a very small table,
results are likely to be paged in SQL Server’s memory for the fastest
performance. When the polling component detects a change in the table,
it will invalidate the linked cache item with the data used by the
application.
Note
To implement database dependencies in ASP.NET 1.x, you start by creating a custom CacheDependency class along the lines of the CacheDependency
class available in ASP.NET 2.0 and beyond. This abstract class will
start a timer in the constructor and call an overridable method—say, HasChanged—to check for changes. The user-defined DatabaseCacheDependency class inherits from CacheDependency and overrides HasChanged
to query against the helper table of change notifications (or
checksums). To insert data in the cache bound to this dependency object,
you resort to a helper method that extends the Insert method of the native Cache
object. Basically, your helper insert method will create a pair of
cache entries—one for the real data, and one for the dependency object
polling for data. The two entries are linked so that changes to the
entry with the dependency invalidate the one with real data. Details and
sample code are available at http://msdn.microsoft.com/msdnmag/issues/04/07/CuttingEdge. |
Enabling Database Dependencies
In ASP.NET 2.0 and beyond, database dependencies are implemented through the SqlCacheDependency
class. The class works with SQL Server 7.0, SQL Server 2000, and the
newer SQL Server 2005. To make it work with SQL Server 2005, much less
setup work is required. Let’s tackle SQL Server 7.0 and SQL Server 2000
first.
For the SqlCacheDependency
class to work correctly, any tables that you want to monitor for
changes must have notifications enabled. Enabling notifications entails
administrative changes to the database that must be accomplished before
the application is published. Changes include creating ad hoc triggers
and stored procedures to handle any incoming UPDATE, INSERT, or DELETE
statements.
You use the command-line tool aspnet_regsql
to do any required offline work. You first enable notifications on the
database, and next do the same on one or more of the database tables.
Run the following command to enable notifications on the Northwind
database for the local installation of SQL Server:
aspnet_regsql.exe -S (local) -U YourUserName -P YourPassword
-d Northwind -ed
Run the following command to enable notification on the Customers table:
aspnet_regsql.exe -S (local) -U YourUserName -P YourPassword
-d Northwind –et –t Customers
The first command adds a new table to the database whose name is AspNet_SqlCacheTablesForChangeNotification.
In addition, a bunch of stored procedures and triggers are added. Note
that you need to specify a login with enough permissions to perform all
the operations.
The second command adds a trigger to the specified table and a new record to AspNet_SqlCacheTablesForChangeNotification that refers to the specified table. Here’s the trigger:
CREATE TRIGGER dbo.[Customers_AspNet_SqlCacheNotification_Trigger]
ON [Customers]
FOR INSERT, UPDATE, DELETE AS BEGIN
SET NOCOUNT ON
EXEC dbo.AspNet_SqlCacheUpdateChangeIdStoredProcedure N'Customers'
END
Figure 5 provides a view of the structure of the change notification table. This table contains one record for each monitored table.
The trigger executes the following stored procedure whenever a change occurs on the monitored table. As a result, the changeId column for the table is modified.
BEGIN
UPDATE dbo.AspNet_SqlCacheTablesForChangeNotification WITH (ROWLOCK)
SET changeId = changeId + 1
WHERE tableName = @tableName
END
To finalize the setup of SqlCacheDependency, you need to add the following script to the application’s web.config file:
<system.web>
<caching>
<sqlCacheDependency enabled="true" pollTime="1000" >
<databases>
<add name="Northwind" connectionStringName="NWind" />
</databases>
</sqlCacheDependency>
</caching>
</system.web>
The pollTime
attribute indicates (in milliseconds) the interval of the polling. In
the preceding sample, any monitored table will be checked every second.
Under the <databases> node, you find a reference to monitored databases. The name attribute is used only to name the dependency. The connectionStringName attribute points to an entry in the <connectionStrings> section of the web.config file and denotes the connection string to access the database.
Note
In addition to using the aspnet_regsql
command-line tool, you can use a programming interface to create the
runtime environment that allows database cache dependencies for SQL
Server 7 and SQL Server 2000. The following code enables the Northwind
database for notifications: SqlCacheDependencyAdmin.EnableNotifications("Northwind");
You add a table to the list of monitored tables with the following code: SqlCacheDependencyAdmin.EnableTableForNotifications(
"Northwind", "Employees");
The SqlCacheDependencyAdmin class also has methods to disable previously enabled dependencies. |
Let’s see now how to create and use a SqlCacheDependency object.
Taking Advantage of SQL Server Dependencies
The SqlCacheDependency class has two constructors. The first takes a SqlCommand object, and the second accepts two strings—the database name and the table name. The constructor that accepts a SqlCommand
is intended for use only with SQL Server 2005; the other is designed
for dependencies that involve older versions of SQL Server. Functionally
speaking, the two are equivalent.
The following code creates a SQL Server dependency and binds it to a cache key:
protected void AddToCache(object data)
{
string database = "Northwind";
string table = "Customers";
SqlCacheDependency dep = new SqlCacheDependency(database, table);
Cache.Insert("MyData", data, dep);
}
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
// Get some data to cache
CustomerCollection data = Customers.LoadByCountry("USA");
// Cache with a dependency on Customers
AddToCache(data);
}
}
The data in the cache can be linked to any data-bound control, as follows:
CustomerCollection data = null;
object o = Cache["MyData"];
if (o != null)
data = (CustomerCollection)o;
else
Trace.Warn("Null data");
CustomerList.DataTextField = "CompanyName";
CustomerList.DataSource = data;
CustomerList.DataBind();
When the database is updated, the MyData entry is invalidated and, as for the sample implementation provided here, the listbox displays empty.
Important
You get notification
based on changes in the table as a whole. In the preceding code, we’re
displaying a data set that results from the following: SELECT * FROM customers WHERE country='USA'
If, say, a new record is added to the Customers table, you get a notification no matter what the value in the country column is. The same happens if a record is modified or deleted where the country column is not USA. SQL
Server 2005 offers a finer level of control and can notify applications
only of changes to the database that modify the output of a specific
command. |
Once you are set up for table notifications, pages that use a SqlDataSource control can implement a smarter form of caching that monitors the bound table for changes and reloads data in case of changes:
<asp:SqlDataSource ID="SqlDataSource1" runat="server"
ConnectionString="<%$ ConnectionStrings:NWind %>"
SelectCommand="SELECT * FROM Customers"
EnableCaching="true"
SqlCacheDependency="Northwind:Customers">
</asp:SqlDataSource>
You set the SqlCacheDependency property to a string of the form Database:Table. The first token is the name of the database dependency as set in the <databases>
section. The second token is the name of the table to monitor. You can
also define multiple dependencies, separating each pair with a
semi-colon:
<asp:SqlDataSource ID="SqlDataSource1" runat="server"
EnableCaching="true"
SqlCacheDependency="Northwind:Customers;Pubs:Authors"
...
/>
Note that caching must be enabled for the feature to work.
Note
Although I’ve mentioned only SqlDataSource, the SqlCacheDependency property also works with ObjectDataSource as long as ObjectDataSource returns data through ADO.NET objects. |
Cache Dependencies in SQL Server 2005
As mentioned, the SqlCacheDependency class has two constructors, one of which takes a SqlCommand object as its sole argument. This constructor is used to create SqlCacheDependency objects for SQL Server 2005 databases. Here’s how to use it:
protected void AddToCache()
{
SqlConnection conn = new SqlConnection(
ConfigurationManager.ConnectionStrings["NWind05"].ConnectionString);
SqlCommand cmd = new SqlCommand(
"SELECT * FROM Customers WHERE country='USA'",
conn);
SqlDataAdapter adapter = new SqlDataAdapter(cmd);
DataTable data = new DataTable();
adapter.Fill(data);
SqlCacheDependency dep = new SqlCacheDependency(cmd);
Cache.Insert("MyData", data, dep);
}
Note that with SQL Server 2005 no setup work is
needed and no external objects must be added to the database. No
triggers, stored procedures, and notification tables are needed.
SQL Server 2005 incorporates a made-to-measure
component that monitors changes at a finer level than was possible in
earlier versions. This component takes a command object and tracks all
the ongoing changes to detect whether something happened to modify the
result set returned by the command. When this happens, the component
pushes new information to the listening object.
So when you instantiate a SqlCacheDependency object and have it guard a given command, a SqlDependency object wraps the command and fires an event when a change is detected. In turn, the SqlCacheDependency catches the event and invalidates the cached item. Figure 6 illustrates the data flow.
Note
The
SQL Server 2005 implementation of database cache invalidation is
clearly the best possible because it leverages a new infrastructure
built in the database. ASP.NET applications receive an asynchronous
notification of changes, which is good for performance and poses no
blocking issues. Also, no setup is necessary for the feature to work.
The SQL Server 7.0 and SQL Server 2000 implementation of the same
feature relies on polling and requires some setup work. The ASP.NET team
made the SQL dependency setup as smooth as possible, but it still
requires an administrative login to get into the database to create
triggers, tables, and stored procedures. This might be a problem if
you’re not allowed full access to the database or if you’re working in
an ISP scenario. Is polling the best possible option for detecting
changes? All things considered, polling is a necessity when there’s no
built-in notification mechanism in the database. And polling a small
table is more efficient than repeatedly running a query. Finally,
polling doesn’t suffer from blocking issues as do approaches built on
extended stored procedures, and it also works great in Web farms and Web
gardens. In the end, the ASP.NET team determined that polling was the
best option for the broadest number of ASP.NET applications willing to
support database cache invalidation. |