Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Distributed Cache - Simplified

0.00/5 (No votes)
22 May 2009 1  
Cache mechanism is one of the basic infrastructures in every medium & above project, either web application or winForm application.

Following the Google search engine or advertising in technology sites or magazines, we can easily find a lot of sophisticated solutions for caching.
I found/know: MemCached, NCache, ScaleOut StateServer,
Shared Cache & even a Microsoft's implementation named Velocity.

Sometimes these solutions are the best thing for your project, they have a lot of bells and whistles, they were written by professionals who dedicated a lot of thought in all sorts of cache situations (or states..) & some of them already proved themselves in production environment - So why invent the wheel?
But...
If you think about it...cache is really simple - at least the basics, so why not take full control?

Cache mechanism is one of the basic infrastructures in every medium & above project, either web application or winForm application.
The main goal of caching is to save roundtrips, from client to server, from application server to database server & in some cases from web server to application server.
The last one (web server to application server) is mainly used in web-sites to save HTML result of common queries instead of redirecting these again & again to the application.
In this article, I will concentrate on the other two.

In every application, there are lookup tables (mainly to fill combo-boxes with list of values), decision tables and other static or semi-static tables, these tables are read again & again whenever a form in the application containing them is opened and/or the application flow needs its values - these roundtrips from client to server & from application server to database are a waste of resources that could be saved if you save these in the client's & application server's memory.

As first step, you need to decide how far you can go with this.

  1. Which static tables are commonly used?
  2. How big are they? (memory on server & client are not endless...).
  3. What kind of access is needed to these tables? (if you search a lot in these tables using joins, it won't be very efficient to cache them).
  4. If these tables are tables that the user can update, how critical is it to refresh them & if so - how often is sufficient?

The answers to these questions are different in every application, but the main guidelines are to cache most (if not all) small static tables that are used in the application & never to cache big tables, tables that are updated often or tables that you can't 'live' with the fact that you're querying an old snapshot of the real one (I'm talking about a few seconds old..).
You are left with the question what is the limit of medium tables (in size) & what is often updated - as said before these change from application to application.

So let's start building...

We'll start with the database:

  1. sysApplicationServers table: This table will function as a registration table, every application server on load will 'register' itself here and will unregister itself when unloaded. Columns: IpAddress, FromDate.
  2. CacheItemQueue table: This table will contain tables needed for refresh.
  3. trig[table name]Cache trigger: Every dynamic & cached table will have a trigger, this trigger will add a row in CacheItemQueue for every sysApplicationServers row (on insert, update & delete).

For example: We have two application servers registered in sysApplicationServers, when we update users table, the result of the trigger is a simple insert with the result:

Table, IpAddress, FromDate
-------------------------------------------
Users, appServer1IP, now
Users, appServer2IP, now

So this simple mechanism will 'let us know' when a table is modified so we can refresh it in memory snapshot (the insert will insert a new row only if there is no existing row for the same table+server combination).

Next, we'll write a thread that will sample CacheItemQueue table in the required interval, this thread will run in an endless loop from application load.
When it identifies a new table for it to load, it loads and deletes this row from cacheItemQueue.

C#
            ...
    CacheListenerThread cacheListenerThread = new CacheListenerThread();
    thread = new Thread(cacheListenerThread.RunListener);
    thread.Start();
    
    while (true)
    {
        Thread.Sleep(Convert.ToInt32
	(AppConfigManager.GetAppSettingsValue("CacheRefreshInterval")));
        RefreshCache();
    }
    
public void RefreshCache()
{
    string ipAddress = BasicUtil.GetLocalIp();
    SqlCommand command = new SqlCommand("spCache_AsyncTablesLoader");
    DatabaseManager.AddSqlParameter(command, "@ipAddress", ipAddress);
    
    RefreshCacheInnerImpl(command);
}
...

The actual cache can be built using ASP.NET which has a nice implementation, I chose Microsoft's Enterprise library to allow the cache to work also under a non web application server (in my case Windows service).

Cache manager interface explains itself:

C#
public interface ICacheManager
{
    object Get(string key);
    bool Add(string key, object value);
    bool Contains(string key);
    void LoadDataTable(string tableName);
}

LoadDataTable method will allow us to maintain cache tables that are loaded only on first use or reloaded if needed.

I mainly use this infrastructure to contain all sorts of dataTables but as you can see, it's built to contain any object & also cache stuff with expiration date/time.
The Server implementation of cache manager:

C#
..
using Microsoft.Practices.EnterpriseLibrary.Caching;
using Microsoft.Practices.EnterpriseLibrary.Caching.Expirations;
..

    public class ServerCache : ICacheManager
    {
        public delegate bool LoadDataTableDelegate(string tableName);

        private static ServerCache serverCacheManager;
        private CacheManager cacheManager;

        private event LoadDataTableDelegate loadDataTableEvent;

        private ServerCache()
        {
            try
            {
                cacheManager = CacheFactory.GetCacheManager();
            }
            catch (Exception ex)
            {
                throw new ApplicationException("Failed to initilize cache manager", ex);
            }
        }

        public static void InitCache(LoadDataTableDelegate loadDataTable)
        {
            serverCacheManager = new ServerCache();
            serverCacheManager.loadDataTableEvent += loadDataTable;
        }

        /// <summary>
        /// get the server cache in a lazy fashion.
        /// </summary>
        /// <returns></returns>
        public static ServerCache GetServerCache()
        {
            if (serverCacheManager == null)
            {
                string message = "cache was not loaded (should call InitCache)";
                throw new ApplicationException(message);
            }
            return serverCacheManager;
        }

        /// <summary>
        /// get value from the cache by the given key
        /// </summary>
        /// <param name=""key""></param>
        /// <returns></returns>
        public object Get(string key)
        {
            return cacheManager.GetData(key);
        }

        /// <summary>
        /// check if object with given key exists in cache
        /// </summary>
        /// <param name=""key""></param>
        /// <returns></returns>
        public bool Contains(string key)
        {
            return cacheManager.Contains(key);
        }

        /// <summary>
        /// add item to cache
        /// </summary>
        /// <param name=""key""></param>
        /// <param name=""value""></param>
        /// <returns>true if the key was overriden</returns>
        public bool Add(string key, object value)
        {
            //if the key already exist - run the value over and return false
            bool result = (cacheManager.Contains(key));
            cacheManager.Add(key, value);
            return result;
        }

        /// <summary>
        /// add item to cache with timeout
        /// </summary>
        /// <param name=""key""></param>
        /// <param name=""value""></param>
        /// <param name=""expirationTime""></param>
        /// <returns>true if the key was overriden</returns>
        public bool Add(string key, object value, TimeSpan expirationTime)
        {
            //check if there is an object which is already cached with the same key
            bool result = (cacheManager.Contains(key));
            cacheManager.Add(key, value, CacheItemPriority.Normal,
				null, new SlidingTime(expirationTime));
            return result;
        }

        public void LoadDataTable(string tableName)
        {
            loadDataTableEvent(tableName);
        }
    }

The client implementation of ICacheManager is even simpler, it holds a static dictionary of objects, the LoadDataTable method can point to the server's gateway delegate or can be left alone if you download only static tables to client side.

C#
public class ClientCache : ICacheManager
{
    private static ClientCache clientCacheManager;
    
    private static Dictionary<string,> cacheMap;
    
    private ClientCache()
    {
        cacheMap = new Dictionary<string,>();
    }
    
    public static ClientCache GetClientCache()
    {
        if (clientCacheManager == null)
        {
            clientCacheManager = new ClientCache();
        }
        return clientCacheManager;
    }
    
    public object Get(string key)
    {
        object result;
        cacheMap.TryGetValue(key, out result);
        return result;
    }
    
    /// <summary>
    /// check if object with given key exists in cache
    /// </summary>
    /// <param name=""key""></param>
    /// <returns></returns>
    public bool Contains(string key)
    {
        return cacheMap.ContainsKey(key);
    }
    
    public bool Add(string key, object value)
    {
        bool overrideKey = cacheMap.ContainsKey(key);
        if (overrideKey)
        {
            lock (cacheMap)
            {
                cacheMap.Remove(key);
                cacheMap.Add(key, value);
            }
        }
        else
        {
            cacheMap.Add(key, value);
        }
        return overrideKey;
    }
    
    public void LoadDataTable(string tableName)
    {
        string message = 
	string.Format("table {0} was not loaded to client cache", tableName);
        throw new ValidationException(message);
    }
}

To allow the cache to be configured except the interval we sample the cacheItemQueue we use a simple XML that contains the list of tables to be cached.
Every element in this XML contains three attributes (except the name of the table of course):

  1. loadOnStart: load on application load or on first call.
  2. loadToClient: include table in the response to client's "getCache" method on client's load.
  3. refreshOnUpdate: if a cache table can be updated, this will be true (to be sure all tables that "refreshOnUpdate" have a trigger, we have a deployment utility that uses this same XML to automatically create these triggers and we check this matches on application load).

If I summarize the main idea:

  1. On application load, the cache data is retrieved to application server's memory, the server registers itself to receive updates & starts the thread checking for updates.
  2. Each dynamic table in cache has a trigger that is used to eventually notify the application server about the update and force it to refresh, the refresh could be made to the whole table (usually small tables with small number of writes) or you can use a timestamp column to identify which rows were updated and selectively refresh the cache.
  3. Every client on load retrieves a snapshot of the static cached tables to save roundtrips to server.

That's it for now, till next time...

Diego

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here