Product Engineer, CTO & a Beer Enthusiast
Experiments, thoughts and scripts documented for posterity.
Jan, 2010
I would probably assume that anyone who is or has been a web-developer would have at some point used ASP.NET caching feature. A very powerful but yet so easily implementable. As a web-developer one fine day I decided to start caching all common objects on the website such as Auto-complete data, Rewritten urls and other database intensive objects/functionality. Though it worked fine and helped us reduce our DB load by more than 30-40% but unfortunately all these 1000's of objects started building up private bytes (memory) on the server and ended up recycling IIS (App recycle) more often than before (mainly because of out-of-memory exception). Just to confirm the extent of memory build-up I wrote a simple CacheViewer handler and after 20mins of data there were more than 20K objects in the server memory being severed with a very long TTL (time to live)!! Following is a screen shot of the Cache size after 20 mins (extracted Memory dump using DebugDiag and used Tess's DotNetMemoryAnalysis script to extract the information)
public static class LRUCache
{
private static object CacheLocker = new object();
//Holds the Key and the number of hits.
private static Dictionary CacheDictionary = new Dictionary();
private static int CacheTimer = 180; //Cache Time
private static int LRULimit = 10000; //Only Cache 10000 Objects
/// Insert value into the cache
public static void Add(string pKey, object o)
{
Add(pKey, o, CacheTimer);
}
/// Insert value into the cache
public static void Add(string pKey, object o, int pCacheTimer)
{
if (o != null)
{
lock (CacheLocker)
{
//make sure the key is not added if already exists
if (HttpRuntime.Cache[pKey] == null)
{
HttpRuntime.Cache.Insert(pKey, o, null, DateTime.Now.AddMinutes(pCacheTimer), System.Web.Caching.Cache.NoSlidingExpiration, System.Web.Caching.CacheItemPriority.Normal, new CacheItemRemovedCallback(ItemRemovedCallback));
AddToCacheDictionary(pKey);
}
}
}
}
/// Remove Key from Dictionary when the Cache Expires
public static void ItemRemovedCallback(String key, object value, CacheItemRemovedReason removedReason)
{
lock (CacheLocker) {
//Remove from Dictionary once the Cache Expires
if (CacheDictionary.ContainsKey(key)) { CacheDictionary.Remove(key); }
}
}
/// get Total cache hits for that pKey
public static int GetLRUCount(string pKey)
{
if (CacheDictionary.ContainsKey(pKey))
{
return CacheDictionary[pKey];
}
return -1;
}
/// Add to Dictionary
private static void AddToCacheDictionary(string pKey)
{
try
{
//remove least accessed object if the count exceded.
if (CacheDictionary.Count > LRULimit) { Clear(GetLeastAccessedObject()); }
//Only add if the object not already exists in dictionary
if (!CacheDictionary.ContainsKey(pKey)) { CacheDictionary.Add(pKey, 1); }
}
catch (Exception ex)
{
throw ex;
}
}
/// Get pKey from Dictionary which was least accessed
private static string GetLeastAccessedObject()
{
List keys = new List(CacheDictionary.Keys);
int oTempVal = CacheDictionary[keys[0]];
string oTempKey = keys[0];
for (int i = 0; i < keys.Count; i++)
{
if (CacheDictionary[keys[i]] < oTempVal)
{
oTempKey = keys[i];
oTempVal = CacheDictionary[keys[i]];
if (oTempVal == 1) break; //Stop the loop if any object has only one hit. Help in reducing the iteration.
}
}
return oTempKey;
}
/// Remove item from cache
public static void Clear(string pKey)
{
//make sure the object did not expire.
if (HttpRuntime.Cache[pKey] != null) { HttpRuntime.Cache.Remove(pKey); }
//remove from Dictionary
if (CacheDictionary.ContainsKey(pKey)) { CacheDictionary.Remove(pKey); }
}
/// Check for item in cache
public static bool Exists(string pKey)
{
if (HttpRuntime.Cache[pKey] != null)
{
if (CacheDictionary.ContainsKey(pKey))
{
CacheDictionary[pKey] = CacheDictionary[pKey] + 1; //increment cache hits
}
return true;
}
return false;
}
/// Get Cached item
public static object Get(string pKey)
{
if (Exists(pKey))
{
return HttpRuntime.Cache[pKey];
}
return null;
}
}