Synchronizing Cache Access in ASP.NET


This article was contributed by ChandraMohan Lingam.

Environment: ASP.NET, C#

Introduction

Have you ever used ASP.NET Application data caching functionality? Have you run into situations when data in the cache was inconsistent or was just not what you expected? The chances are you are running into synchronization issues when using the cache. An ASP.NET cache object has an application-wide scope and allows you to share data with several instances of the same page or multiple pages across the application. To be beneficial, the cache needs to be accessible to all the pages and to be really useful, there should be some control on who is updating the cache. If too many threads update the contents of a particular key, data will be corrupted.

Sample Application Install

  1. Unzip CacheSync.zip to C:InetpubWWWRootCacheSync.
  2. Create a virtual directory CacheSync in IIS and point to C:InetpubWWWRootCacheSync.
  3. This sample needs access to the NorthWind database. Update the connection string in Web.config:
  4. <add key="ConnectionString" value="server=localhost;user
    id=;password=;database=northwind;connection reset=FALSE;"/>
    

Synchronization Issues—Simple Demo (SimpleDemo.aspx)

To demonstrate the synchronization issues, let’s consider a simple example. We are going to store a list of values in the cache from one instance of the page and read the data from another instance of page. Assume that the process to update the cache takes a couple of seconds (to simulate the delay in making the database call and some time-consuming processing of data). When the cache is being updated by one thread, any thread can potentially read inconsistent data.

Open the page SimpleDemo.aspx from two instances of the browser. From one instance, call the “Update Cache” method and from the other instance of the browser, call the “Read” method.


private void btnWrite_Click(object sender, System.EventArgs e)
{
Cache[“Key”] = “empty”;
Thread.Sleep(5000); //simulate a delay
Cache[“Key”] = System.DateTime.Now;
}

private void btnRead_Click(object sender, System.EventArgs e)
{
txtOut.Text = Cache[“Key”].ToString () ;
}

It is clear from the above example that cache data can be in an invalid state. Allowing threads to read this invalid data can result in unpredictable application behavior.

The cache access behavior we want is to allow multiple threads to read the cache simultaneously but restrict the write access to one thread.

Solution 1—Lock the Writes (SynchronizationLockWrites.aspx)

Let’s bring some order to the write operation. The .NET framework provides a synchronization primitive called “lock”. This allows a thread to acquire a lock on an instance of an object. What this means is that only one thread can acquire a lock on the object and other threads have to wait until the original thread releases the lock. Now, let’s modify the cache update code to include locking. This sample pulls a list of product names from the Northwind database after locking the cache object. Please update the connection string.

Note: Acquiring a lock directly on the caching object is not recommended and will prove to be disastrous in terms of performance.


private void UpdateCacheFromDB()
{
lock(Cache)
{
Cache.Remove (“Key”);
ArrayList arData = new ArrayList ();

// Read from northwind database and update arData arraylist.

// Simulate a delay
Thread.Sleep (5000);
Cache.Insert (“Key”, arData);
}
}

If you run the SynchronizationLockWrites.aspx sample again, you will notice that we haven’t eliminated the problem yet. All we have made sure of is that only one thread can call the cache update method and other update calls will be queued. When some thread is writing in the cache, it is still possible for other threads to read the data.

So, let’s redefine our cache behavior:

  1. Multiple threads can read cache data simultaneously.
  2. Only one thread can update the cache at any time.
  3. When a thread is updating the cache, all the threads that want to read the cache data should wait until the update is complete.

Solution 2—Reader/Writer Locks (SynchronizationReaderWriter.aspx)

The .NET framework provides another thread synchronization primitive called Reader Writer locks. If you look at MSDN help for reader/writer locks, it “Defines the lock that implements single-writer and multiple-reader semantics.” It is perfect because it meets all our requirements. It allows simultaneous reads and one write. And when a write is happening, no other thread can read the cache. Now, let’s rewrite the cache update and cache read methods.


private void UpdateCache()
{
try
{
// Acquire writer lock
rwl.AcquireWriterLock (10000);
try
{
if (!CacheExpired)
{
return;
}

Cache.Remove (“Key”);
ArrayList arData = new ArrayList ();


// Read from northwind database and update arData arraylist.

// Simulate a delay. Remove in production code
Thread.Sleep (5000);
Cache.Insert (“Key”, arData);

LastRefreshTime = System.DateTime.Now;
}

finally
{
// Release writer lock
rwl.ReleaseWriterLock ();
}
}
catch (Exception ex)
{
throw ex;
}
}

// Cache Read method
private void btnRead_Click(object sender, System.EventArgs e)
{
if (CacheExpired)
{
UpdateCache();
}

try
{
rwl.AcquireReaderLock (10000);
try
{

// Read cache

}

finally
{
rwl.ReleaseReaderLock ();
}
}

}

// Verify whether the cache data is stale
private bool CacheExpired
{
get
{
DateTime currentTime = System.DateTime.Now;
TimeSpan ts = currentTime.Subtract (LastRefreshTime);

return (ts.TotalSeconds < 0 || ts.TotalSeconds >
ApplicationConfiguration.RefreshInterval);
}
}

This solution uses a time stamp to determine when the cache was last refreshed. It uses a web.config “CacheRefreshInterval” configuration to determine whether the cache is stale. You may wonder whether you really need to store the time stamp information. Why not use cache expiration policy that is available in the cache framework? The problem with that approach is that you have to really look at the cache to determine whether the cache has data for a specific key. This will once again open up a can of worms because some other thread can potentially be in the process of updating the values for the key.

Now, let’s run the sample again. If you click on the “update cache” method from one page and click on “read cache” from another page, the read page actually waits for the update to complete. This is perfect because the cache access is totally controlled and the data is guaranteed to be valid (assuming all downstream databases and applications are working correctly). This implementation is much robust and the read method automatically refreshes the cache if the data is stale.

But, the big problem in this approach is that the code is really messy and getting too complex. In addition, you need to make sure the locks are acquired and released correctly. If you don’t release the lock properly or acquire locks in the wrong order, the application will deadlock and go into an inconsistent state.

If you are using a cache to store different types of data (for example, a list of products, a list of sales tax by state, and so forth), you really have to use different instances of reader/writer locks to make the application scalable. If you use one instance of reader/writer lock to control access to all the cache data, a thread updating product list will force a thread reading sales tax by state to wait.

One option is to move all this cache update and read logic to a base class and derive all the pages from the base class. The problem in taking this approach is that the base class will get really heavy and any change to the base class can potentially have a negative impact on the other pages.

Solution 3—Proper Locking Using a Lock Construct (SynchronizationLock.aspx)

Let’s consider another solution for solving this problem. The previous solution was too complex to use. Let’s attempt a simpler solution using locks. In this approach, we are going to use locks and timestamps to control access to the cache. We use flags to verify whether the cache has expired; if it has, the update cache method will be called.

In this solution, we are going to acquire a lock on a static string variable when updating the cache. The only purpose of this string variable is to help in synchronization. It is much more scalable than acquiring locks directly on the cache object.


private void UpdateCache()
{
lock(lockString)
{
if (!CacheExpired)
{
return;
}

Cache.Remove (“Key”);
ArrayList arData = new ArrayList ();


// Read from Northwind database and update arData arraylist.

// Simulate a delay. Remove in production code
Thread.Sleep (5000);
Cache.Insert (“Key”, arData);
LastRefreshTime = System.DateTime.Now;
}
}

private void btnRead_Click(object sender, System.EventArgs e)
{
if (CacheExpired)
{
UpdateCache();
}

object obj = Cache.Get (“Key”);
if (obj == null)
txtOut.Text = “Cache is empty”;
else
{
txtOut.Text = “”;
lstOutput.DataSource = (ArrayList) obj;
lstOutput.DataBind ();
}
}

This option behaves similarly to the previous solution, but with reduced complexity.

Solution 4—Using Utility Classes and an External Cache (SynchronizationConsumer.aspx)

Using an ASP.NET cache has a drawback; that drawback is that you can use the cache only for Web applications. If you have a Windows service or a window application, you need some other mechanism to maintain the cache information.

The Microsoft Caching Application block is a potential solution that will work for a variety of .NET solutions. The problem with the MS Caching application block is that it carries a lot of overhead and has a lot of features that may not be useful for your application. So, if you want a lightweight caching layer that offers high performance at the cost of limited features, march ahead because we are going to build one. This approach moves the cache synchronization logic to a dedicated class and the consumers don’t have to worry about synchronization issues.

The generic singleton cache class bizCache is implemented in bizCache.cs. The bizProducts class is responsible for pulling data from the Northwind database and keeping the cache up to date. It is also responsible for handling all synchronization issues. This class is implemented in bizProducts.cs. Any .NET application that wants to use the bizProducts class can do so with a single line of code and not worry about synchronization issues.


// Sample consumer
private void btnRead_Click(object sender, System.EventArgs e)
{
lstOutput.DataSource = bizProducts.GetInstance
().GetproductTypes ();

lstOutput.DataBind ();
}

Here we have consolidated all the caching logic into a class. This class does not depend on ASP.NET and can be used in all .NET applications.

Conclusion

Using a cache can dramatically improve performance and scalability. But, using a cache brings its own bag of problems. With proper synchronization techniques, you can avoid mysterious cache-related bugs.

About the Author

ChandraMohan Lingam is a Senior Application Developer at Intel Corporation.

Downloads

Download source – 28Kb

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read