Synchronizing Cache Access in ASP.NET

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

Unzip CacheSync.zip to C:\Inetpub\WWWRoot\CacheSync.

Create a virtual directory CacheSync in IIS and point to C:\Inetpub\WWWRoot\CacheSync.

This sample needs access to the NorthWind database. Update the connection string in Web.config:

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.

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.

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:

Multiple threads can read cache data simultaneously.

Only one thread can update the cache at any time.

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.

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.

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.

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.

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.

Dependancy Problem

Posted by notoriousvic
on 05/03/2004 10:31am

I am trying to store directory names in the cache. I have over 500 directories in 3 folders. I have tried using Dependencies for directories and I noticed that the cache would expire if someone accessed the folder. The best scenario would be to expire the cache when the folder LastWrittenTime has changed. I can't seem to do this because I cannot inherit the cache dependencies class.
Any Suggestions?

re: What is the real problem with SimpleDemo.aspx? -

The problem that I am explaining with simple demo is:
If the cache has expired, it takes a finite amount of time for the application to update the cache. When one thread of the application is updating the cache, what do we do with read requests from other threads? Do we serve old content (knowing the cache is stale) or do we force the read process to wait until the cache is up-to-date. If we decide to serve the old content, do we still have the old content? If we still have old content, when do we switch from old content to new content? These are some of the details that's going to bite us in a production environment. We need a clear understanding and control when cache data becomes stale, how do we update stale data, how to handle read requests when stale data is being updated and so on.

This code sets the value of the cache twice for the same key, so indeed when you click on the "Read" button you will see the two values one after another.
System.Web.Caching.Cache has locking built in and is safe to be accessed from different threads. This example does not demonstrate any cache contention as far as I see it.

I would really want to see if the cache application block has any benefits over the ASP.NET Cache object for ASP.NET apps that do not require flexibility in using different storages, or can live with the single appdomain scope limitation.

Any ideas? Does the ASP.NET have an access problem like implied in this article?

Top White Papers and Webcasts

Live Event Date: March 19, 2015 @ 1:00 p.m. ET / 10:00 a.m. PT
The 2015 Enterprise Mobile Application Survey asked 250 mobility professionals what their biggest mobile challenges are, how many employees they are equipping with mobile apps, and their methods for driving value with mobility.
Join Dan Woods, Editor and CTO of CITO Research, and Alan Murray, SVP of Products at Apperian, as they break down the results of this survey and discuss how enterprises are using mobile application management and private …

Today's agile organizations pose operations teams with a tremendous challenge: to deploy new releases to production immediately after development and testing is completed. To ensure that applications are deployed successfully, an automatic and transparent process is required. We refer to this process as Zero Touch Deployment™. This white paper reviews two approaches to Zero Touch Deployment--a script-based solution and a release automation platform. The article discusses how each can solve the key …