I develop for a large, high-availability website, with hundreds of thousands of daily users. As such, we need to cache a lot of data in our web-server memory (which is cheap) to save numerous hits to our main database cluster (which is very expensive). I would imagine the desire to improve performance by saving on database hits is common across many web applications – and caching frequently used data is often seen as one of the best ways to solve this problem.
There are two further specific problems I face in every-day life, and these are:
- We have lots of silly little objects and lists of objects we wish to cache to improve performance
- Populating the cache (on request) needs to be thread-locked
Why thread locked?
When the loading of a particular cached item is particularly expensive, say, a fifteen-second database query, there is the possibility that multiple requests can try to populate the cache at the same time – so for example, if ten people sdoes it imultaneously request a resource, with something like below – you can run into problems due to the required null check:
public List<int> CachedWidgetIds
{
get
{
if (Cache["CachedWidgetIds"] == null)
{
Cache["CachedWidgetIds"] = SomeProvider.GetWidgetIds();
}
return (List<int>)Cache["CachedWidgetIds"];
}
}
If this property is accessed more than once quickly, and if SomeProvider.GetWidgetIds(); takes a few seconds to complete – there is every possiblity that multiple requests will attempt to populate this cached object at the same time, until at least one of these completes and assigns a value to the cache.
At a database level, this could potentially cause row-locking and deadlocks if multiple requests are all needlessly running the same expensive query, meaning the query could fail thus making the problem much worse.
In order to solve these problems, and standardize or cache access, we have implemented a generic class as follows:
public interface ICachable
{
string GetUniqueCacheName();
int GetCacheTime();
void FillSelf();
}
public static class SafeCache<T> where T : ICachable
{
private static T singletonObject;
private static object classLock = new object();
public static T Object
{
get { return singletonObject == null ? CacheMethod() : singletonObject; }
}
public static T CacheMethod()
{
if (HttpContext.Current == null)
{
// if the current httpcontext is null then we are in a non-web app and can't use web caching
if (singletonObject == null)
{
singletonObject = Activator.CreateInstance<T>();
singletonObject.FillSelf();
}
return singletonObject;
}
else
{
//Create an instance of a generic type T for a reference type.
//Using default(T) will return null
T cachedObject = Activator.CreateInstance<T>();
if (HttpContext.Current.Cache[cachedObject.GetUniqueCacheName()] != null)
{
cachedObject = (T)HttpContext.Current.Cache[cachedObject.GetUniqueCacheName()];
}
else
{
lock (classLock)
{
//Why check if the cache is null for a second time?
//If person #1 enters this statement, they will lock "classLock" and start
//to run cachedObject.FillSelf(); - the lock will mean any requests that take
//place during the time it takes to exectue cachedObject.FillSelf() operation
//will wait at the line "lock (classLock)" - then, once Person #1 releases the lock,
//they'll all come pouring into this satement - and we dont want them to run FillSelf() again!
if (HttpContext.Current.Cache[cachedObject.GetUniqueCacheName()] != null)
{
cachedObject = (T)HttpContext.Current.Cache[cachedObject.GetUniqueCacheName()];
}
else
{
cachedObject.FillSelf();
HttpContext.Current.Cache.Insert(cachedObject.GetUniqueCacheName(), cachedObject, null,
DateTime.Now.AddHours(cachedObject.GetCacheTime()), TimeSpan.Zero);
}
}
}
return cachedObject;
}
}
}
This class can be used with any object that implements the simple interface “ICacheable” (decalred above) to provide a standard way of populating itself with data. So, to make a long story short, this can be used like:
DropDownList1.DataSource = SafeCache<Widgets>.Object.WidgetIds;
DropDownList1.DataBind();
As you can see, this removes all the workings of the cache from the calling resource, and provides a clean and thread-safe way for it to be accessed.