DoubleCache, https://github.com/AurumAS/DoubleCache, is my own open source implementation of a layered distributed cache, it builds upon solid projects like Redis, StackExchange.Redis and MsgPack and combines these with a local cache implementation on the .Net stack.

Some history

We're already running a similar solution with my current customer, NRK. Our motivation for creating this, came after migrating from Azure Managed Cache to Azure Redis Cache. Azure Managed cache has the nice feature of a local cache on the client, in front of the Managed cache. This is not available when using Redis, as Microsoft has not created their own client, instead they recommend using the (excellent) StackExchange.Redis client. When we moved to Redis, our CPU went haywire as we did a LOT of cache requests (often way too many, it's hard to notice when you hit local memory) and used BinaryFormatter for serialization. Besides cleaning up our data access, we needed our own layered cache. Our layered implementation over StackExchange.Redis is quite OK, however it is a bit tangled with old interfaces and not something which can be easily reused by others. DoubleCache aims to fix this.

My goals when creating DoubleCache

I created DoubleCache for a meetup where I talked about the penalties of moving to a remote cache, and I needed some code to highlight the problems with remote caches. As I wanted a clean implementation and other open source projects were too messy, I decided to implement my own with the following criteria

  1. It should be possible to use as a local, remote or local and remote cache - with or without sync, through a single interface. Changing from a local to a remote cache should not require any changes in the client using it besides swapping interface implementation.
  2. Each implementation should in it self be trivial
  3. Follow the cache aside pattern
  4. Extending the functionality should not require any modifications to existing imeplementations. Using standard patterns such as decorator ought to do it.
  5. Serialization must be pluggable.

Usage

Add a reference to DoubleCache using nuget Install-Package DoubleCache and initialize the DoubleCache with a remote and a local cache.

var connection = ConnectionMultiplexer.Connect("localhost");  
var serializer = new MsgPackItemSerializer();

_pubSubCache = CacheFactory.CreatePubSubDoubleCache(connection, serializer);  

To use the cache call the GetAsync<T> method. This method takes a Func called dataRetriever. This method should call your repository or other service. The dataRetriever method executes if the requested key does not exist in the cache, adding the result to the cache.

var cacheKey = Request.RequestUri.PathAndQuery;

pubSubCache.GetAsync(cacheKey, () => _repo.GetSingleDummyUser()));  

Implementation

The ICacheAside interface is the main part of DoubleCache, all variants relies on implementations of this single interface.

    public interface ICacheAside
    {
        void Add<T>(string key, T item);
        void Add<T>(string key, T item, TimeSpan? timeToLive);

        Task<T> GetAsync<T>(string key, Func<Task<T>> dataRetriever) where T : class;
        Task<T> GetAsync<T>(string key, Func<Task<T>> dataRetriever, TimeSpan? timeToLive) where T : class;

        Task<object> GetAsync(string key, Type type, Func<Task<object>> dataRetriever);
        Task<object> GetAsync(string key, Type type, Func<Task<object>> dataRetriever, TimeSpan? timeToLive);
    }

The Add method is implemented with fire and forget, hence it does not need to be Async as this is handled by the Stackexchange.Redis client.

DoubleCache comes with the following implementations of this interface

  • LocalCache.MemCache - using System.Runtime.Memory
  • Redis.RedisCache - using StackExchange.Redis client
  • DoubleCache - a decorator wrapping a local and a remote cache
  • PublishingCache - a decorator publishing cache changes
  • SubscribingCache - a decorator supporting push notifications of cache updates

As seen in the usage example, combining the decorators and implementations provide a high level of flexibility. It comes at a little mental cost when wiring up the DoubleCache constructor. This can be relieved to hide the constructor behind a factory method.

LocalCache.MemCache

This uses the System.Runtime.Caching.MemoryCache.Default instance throughout the implementation.

A word of warning: These are mutable objects stored in memory. Be careful if you modify items retrieved from the cache. There can be only one.

Redis.RedisCache

A wrapper around StackExchange.Redis. Since the IConnectionMultiplexer should be a single instance, you will need to pass the Redis database in the constructor. The StringSet call to Redis use the FireAndForget option.

DoubleCache

This is a decorator used to wire the local and remote cache together.

To achieve this, it simply calles the local cache first and wraps the remote cache in the dataRetriever function.

return _localCache.GetAsync(key, type, () => _remoteCache.GetAsync(key, type, dataRetriever), timeToLive);  

While DoubleCache will keep the remote cache in sync with any add calls made on the local cache it owns, syncing the local cache with remote cache changes triggered by other clients is not the responsibility of the DoubleCache wrapper. This is covered by the publishing and subscribing cache wrappers.

PublishingCache

A decorator for publishing changes when adding/updating items in the cache. The key and object type is published using the ICachePublisher interface, which then passes the message on to Redis Pub/Sub. As each cache implementation is responsible for adding an item to itself when the dataRetriever function is executed, ICachePublisher wraps this method causing a cache publish after it is executed.

public Task<T> GetAsync<T>(string key, Func<Task<T>> dataRetriever) where T : class  
{
    return  _cache.GetAsync(key, async() => {
        var result = await dataRetriever.Invoke();
        _cachePublisher.NotifyUpdate(key, result.GetType().AssemblyQualifiedName);
        return result;
    });
}

The publishing cache is intended to wrap the remote/central cache implementation.

Subscribing cache

This decorator wraps a cache and through the implementation of ICacheSubscriber it will call Add on the wrapped cache when the ICacheSubscriber.CacheUpdate event is fired. The RedisSubscriber implementation of ICacheSubscriber will also need a reference to the remote cache, as the Pub/Sub message does not contain the cache item itself, only the key and the type.

The Subscribing cache decorator is intended to wrap the local cache.

Serialization

As mentioned in in my post on cache speed, there are a lot of options when it comes to serializers. By default, DoubleCache comes with BinaryFormatter and MsgPack.

If your data-types support it, I highly recommend something other than BinaryFormatter.

To implement another serializer you will need to implement the IItemSerializer interface

    public interface IItemSerializer
    {
        byte[] Serialize<T>(T item);

        T Deserialize<T>(byte[] bytes);
        T Deserialize<T>(Stream stream);
        object Deserialize(byte[] bytes, Type type);
    }

Whats next

I have a few open issues on GitHub which I intend to close:

  1. Passing TimeToLive with pub/sub, or retrieving it from the cache
  2. Creating a factory to make it simpler to get started
  3. Adding delete operations

You'll find DoubleCache over at GitHub https://github.com/AurumAS/DoubleCache