Search code examples
c#azureredisazure-sql-databasebooksleeve

implementing out-of-process cache using Redis in windows azure


I've been working on a webpage that displays a table from a database I have in my azure cloud. In order to reduce calls to the DB directly for performance improvement I would like to build a cache for the page. Currently, I hold an in-memory cache (in-process) for the reads of the table. Now I would like to make an out-of-process cache, that should be updated from when writes are made, meaning inserts or updates (because after a value is updated or added, the in-memory cache will be no longer valid).

I was recommended on Redis, and specifically Book Sleeve, my question is where I can find some code samples to help me figure out how to start build the out-of-process cache with it and combine it in my current project.

Thanks in advance


Solution

  • If you want purely out-of-process, then it is pretty simple - something like the following, but noting that a BookSleeve is designed to be shared: it is fully thread-safe and works as a multiplexer - you shouldn't create / dispose them for every call. Note also that in this context I'm assuming you will handle serialization separately, so I'm simply exposing a byte[] API:

    class MyCache : IDisposable
    {
        public void Dispose()
        {
            var tmp = conn;
            conn = null;
            if (tmp != null)
            {
                tmp.Close(true);
                tmp.Dispose();
            }
        }
        private RedisConnection conn;
        private readonly int db;
        public MyCache(string configuration = "127.0.0.1:6379", int db = 0)
        {
            conn = ConnectionUtils.Connect(configuration);
            this.db = db;
            if (conn == null) throw new ArgumentException("It was not possible to connect to redis", "configuration");
        }
        public byte[] Get(string key)
        {
            return conn.Wait(conn.Strings.Get(db, key));
        }
        public void Set(string key, byte[] value, int timeoutSeconds = 60)
        {
            conn.Strings.Set(db, key, value, timeoutSeconds);
        }
    }
    

    What gets interesting is if you want a 2-tier cache - i.e. using local memory and the out-of-process cache, as now you need cache invalidation. Pub/sub makes that handy - the following shows this. It might not be obvious, but this would be doing a lot fewer calls to redis (you can use monitor to see this) - since most requests are handled out of the local cache.

    using BookSleeve;
    using System;
    using System.Runtime.Caching;
    using System.Text;
    using System.Threading;
    
    class MyCache : IDisposable
    {
        public void Dispose()
        {
            var tmp0 = conn;
            conn = null;
            if (tmp0 != null)
            {
                tmp0.Close(true);
                tmp0.Dispose();
            }
    
            var tmp1 = localCache;
            localCache = null;
            if (tmp1 != null)
                tmp1.Dispose();
    
            var tmp2 = sub;
            sub = null;
            if (tmp2 != null)
            {
                tmp2.Close(true);
                tmp2.Dispose();
            }
    
        }
        private RedisSubscriberConnection sub;
        private RedisConnection conn;
        private readonly int db;
        private MemoryCache localCache;
        private readonly string cacheInvalidationChannel;
        public MyCache(string configuration = "127.0.0.1:6379", int db = 0)
        {
            conn = ConnectionUtils.Connect(configuration);
            this.db = db;
            localCache = new MemoryCache("local:" + db.ToString());
            if (conn == null) throw new ArgumentException("It was not possible to connect to redis", "configuration");
            sub = conn.GetOpenSubscriberChannel();
            cacheInvalidationChannel = db.ToString() + ":inval"; // note that pub/sub is server-wide; use
                                                                 // a channel per DB here
            sub.Subscribe(cacheInvalidationChannel, Invalidate);   
        }
    
        private void Invalidate(string channel, byte[] payload)
        {
            string key = Encoding.UTF8.GetString(payload);
            var tmp = localCache;
            if (tmp != null) tmp.Remove(key);
        }
        private static readonly object nix = new object();
        public byte[] Get(string key)
        {
            // try local, noting the "nix" sentinel value
            object found = localCache[key];
            if (found != null)
            {
                return found == nix ? null : (byte[])found;
            }
    
            // fetch and store locally
            byte[] blob = conn.Wait(conn.Strings.Get(db, key));
            localCache[key] = blob ?? nix;
            return blob;
        }
    
        public void Set(string key, byte[] value, int timeoutSeconds = 60, bool broadcastInvalidation = true)
        {
            localCache[key] = value;
            conn.Strings.Set(db, key, value, timeoutSeconds);
            if (broadcastInvalidation)
                conn.Publish(cacheInvalidationChannel, key);
        }
    }
    
    static class Program
    {
        static void ShowResult(MyCache cache0, MyCache cache1, string key, string caption)
        {
            Console.WriteLine(caption);
            byte[] blob0 = cache0.Get(key), blob1 = cache1.Get(key);
            Console.WriteLine("{0} vs {1}",
                blob0 == null ? "(null)" : Encoding.UTF8.GetString(blob0),
                blob1 == null ? "(null)" : Encoding.UTF8.GetString(blob1)
                );
        }
        public static void Main()
        {
            MyCache cache0 = new MyCache(), cache1 = new MyCache();
            string someRandomKey = "key" + new Random().Next().ToString();
            ShowResult(cache0, cache1, someRandomKey, "Initially");
            cache0.Set(someRandomKey, Encoding.UTF8.GetBytes("hello"));
            Thread.Sleep(10); // the pub/sub is fast, but not *instant*
            ShowResult(cache0, cache1, someRandomKey, "Write to 0");
            cache1.Set(someRandomKey, Encoding.UTF8.GetBytes("world"));
            Thread.Sleep(10); // the pub/sub is fast, but not *instant*
            ShowResult(cache0, cache1, someRandomKey, "Write to 1");
        }
    }
    

    Note that in a full implementation you probably want to handle occasional broken connections, with a slightly delayed reconnect, etc.