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:\Inetpub\WWWRoot\CacheSync.
  2. Create a virtual directory CacheSync in IIS and point to C:\Inetpub\WWWRoot\CacheSync.
  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


Comments

  • GHD Mørke Styler

    Posted by motherdhmm on 05/30/2013 06:40pm

    [url=http://www.buy-beatsdrdre.com/]beats by dre headphones[/url] hårpleje er vigtigt at undgå disse hår betingelser. Ved at spise en velafbalanceret kost, er hår givet sin fulde sundhedsmæssige fordele. Vask håret forsigtigt med shampoo en gang om dagen, lathering blidt, og ikke gnide i håret for meget på håndklæde til at tørre viser sig at være effektive i at tage sig af ens kronen. Undgå brug af hårtørrer så meget som muligt, og stil hår, når det er tør eller fugtig, ikke når de stadig er våde. [url=http://www.blog.cheapbeatsbydre.co.nz/]beats by dre[/url] GHD styling DVD-diske: Brug GHD. IV styler glattejern, denne gave cd-rom giver dig mulighed for deres komfortable hjem for at nyde GHD IV styler salon kvalitet. Billige Ghd Salg Uanset om for begyndere eller gamle kunder, er denne cd den perfekte guide. [url=http://www.blog.cheapbeatsbydre.co.nz/beats-by-dre-headphones]beats by dre headphones[/url] maj menneskelige modstander i planeten væsentligt flere personer ofte nå udfordringer på indersiden tab af hår, for folk se, hår

    Reply
  • 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?

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

    Posted by Legacy on 02/25/2004 12:00am

    Originally posted by: chandra

    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.

    Reply
  • What is the real problem with SimpleDemo.aspx?

    Posted by Legacy on 02/21/2004 12:00am

    Originally posted by: Alex

    I guess I do not understand what is the real problem with the code in the article:

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

    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?

    Reply
  • cache object on web garden

    Posted by Legacy on 11/05/2003 12:00am

    Originally posted by: andre

    isnt it correct that on a multiprocessor machine there is one cache object for every processor?

    see post

    http://groups.google.ch/groups?q=cache+object+web+garden+asp.net&hl=de&lr=&ie=UTF-8&oe=UTF-8&selm=ZeH185wdCHA.1496%40cpmsftngxa09&rnum=1


    in that case one would need to build a custom cache object using a static hashtable or similar collection.


    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: September 17, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT Another day, another end-of-support deadline. You've heard enough about the hazards of not migrating to Windows Server 2008 or 2012. What you may not know is that there's plenty in it for you and your business, like increased automation and performance, time-saving technical features, and a lower total cost of ownership. Check out this upcoming eSeminar and join Rich Holmes, Pomeroy's practice director of virtualization, as he discusses the …

  • Live Event Date: September 16, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Are you starting an on-premise-to-cloud data migration project? Have you thought about how much space you might need for your online platform or how to handle data that might be related to users who no longer exist? If these questions or any other concerns have been plaguing you about your migration project, check out this eSeminar. Join our speakers Betsy Bilhorn, VP, Product Management at Scribe, Mike Virnig, PowerSucess Manager and Michele …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds