Setting up a Connection with StackExchange.Redis

Update 25th October 2016: Just looking to quickly set up a Redis connection? Check out the followup article. Read on for a more detailed article on the topic.

StackExchange.Redis is a pretty good .NET client for Redis. Unfortunately, it can be a little bit tricky to use, and the existing documentation is far from comprehensive.

After installing StackExchange.Redis via NuGet, a Redis connection can be obtained via a special ConnectionMultiplexer object. Working with this is already tricky in itself, and many get this wrong. For instance, check out the implementation in this answer:

public static ConnectionMultiplexer RedisConnection;
public static IDatabase RedisCacheDb;

protected void Session_Start(object sender, EventArgs e)
    {
        if (ConfigurationManager.ConnectionStrings["RedisCache"] != null)
        {
            if (RedisConnection == null || !RedisConnection.IsConnected)
            {
                RedisConnection = ConnectionMultiplexer.Connect(ConfigurationManager.ConnectionStrings["RedisCache"].ConnectionString);
            }
            RedisCacheDb = RedisConnection.GetDatabase();
        }
    }

As I pointed out in my question, this is a bad form of lazy initialization because it lacks thread safety: multiple threads may get through the checks and initialize multiple connections, resulting in connection leaks.

It is not hard to prove that this code is leaky in multithreaded environments. First, let’s set up the ConfigurationOptions with a client name so that we can identify connections coming from our program:

        private static Lazy<ConfigurationOptions> configOptions
            = new Lazy<ConfigurationOptions>(() => 
            {
                var configOptions = new ConfigurationOptions();
                configOptions.EndPoints.Add("localhost:6379");
                configOptions.ClientName = "LeakyRedisConnection";
                configOptions.ConnectTimeout = 100000;
                configOptions.SyncTimeout = 100000;
                return configOptions;
            });

Then, we provide a property with the faulty lazy initialization:

        private static ConnectionMultiplexer conn;

        private static ConnectionMultiplexer LeakyConn
        {
            get
            {
                if (conn == null || !conn.IsConnected)
                    conn = ConnectionMultiplexer.Connect(configOptions.Value);
                return conn;
            }
        }

Finally, we write some code that runs some Redis stuff in parallel:

        static void Main(string[] args)
        {
            for (int i = 0; i < 3; i++)
            {
                Task.Run(() =>
                    {
                        var db = LeakyConn.GetDatabase();
                        Console.WriteLine(i);

                        string key = "key" + i;

                        db.StringSet(key, i);
                        Thread.Sleep(10);
                        string value = db.StringGet(key);
                    }
                );
            }

            Console.WriteLine("Done");
            Console.ReadLine();
        }

When the program does its work, even with just 3 iterations, we get a total of six connections (when normally a single ConnectionMultiplexer should have at most 2 physical connections):

redis-leaky-connections

Another approach from this answer is to use an exclusive lock:

private static ConnectionMultiplexer _redis;
private static readonly Object _multiplexerLock = new Object();

private void ConnectRedis()
{
    try
    {
        _redis = ConnectionMultiplexer.Connect("...<connection string here>...");
    }
    catch (Exception ex)
    {
        //exception handling goes here
    }
}


private ConnectionMultiplexer RedisMultiplexer
{
    get
    {
        lock (_multiplexerLock)
        {
            if (_redis == null || !_redis.IsConnected)
            {
                ConnectRedis();
            }
            return _redis;
        }
    }
}

However, since Redis is often used as a cache in highly concurrent web applications, this approach essentially forces code to degrade into something sequential, and has obvious performance implications.

The correct approach to using ConnectionMultiplexer is described by this answer. It involves use of Lazy<T> for thread-safe lazy initialization (see Jon Skeet’s article on Singletons). Additionally:

  • It sets “abortConnect=false”, which means if the initial connect attempt fails, the ConnectionMultiplexer will silently retry in the background rather than throw an exception.
  • It does not check the IsConnected property, since ConnectionMultiplexer will automatically retry in the background if the connection is dropped.

With this info, we can now fix our code:

        private static Lazy<ConfigurationOptions> configOptions
            = new Lazy<ConfigurationOptions>(() => 
            {
                var configOptions = new ConfigurationOptions();
                configOptions.EndPoints.Add("localhost:6379");
                configOptions.ClientName = "SafeRedisConnection";
                configOptions.ConnectTimeout = 100000;
                configOptions.SyncTimeout = 100000;
                configOptions.AbortOnConnectFail = false;
                return configOptions;
            });

        private static Lazy<ConnectionMultiplexer> conn
            = new Lazy<ConnectionMultiplexer>(
                () => ConnectionMultiplexer.Connect(configOptions.Value));

        private static ConnectionMultiplexer SafeConn
        {
            get
            {
                return conn.Value;
            }
        }

        static void Main(string[] args)
        {
            for (int i = 0; i < 3; i++)
            {
                Task.Run(() =>
                    {
                        var db = SafeConn.GetDatabase();
                        Console.WriteLine(i);

                        string key = "key" + i;

                        db.StringSet(key, i);
                        Thread.Sleep(10);
                        string value = db.StringGet(key);
                    }
                );
            }

            Console.WriteLine("Done");
            Console.ReadLine();
        }

If you run this, you’ll find that there are now only two physical connections generated by the application, which is normal.

redis-safe-connections

13 thoughts on “Setting up a Connection with StackExchange.Redis”

  1. Very Nice Post Gigi.
    One Thing I want’s to ask is about KeepAlive option. Also is above setting is viable for IIS hosted WCF service?

    1. Kamran, did you catch that it’s the wrong solution? It’s not that “yes you can also lazy create the connection” as some sort of an alternative to the options is simply wrong.

  2. Oh yes you can use KeepAlive in a WCF service. Actually, you should do that. The point of ConnectionMultiplexer is to have just one ConnectionMultiplexer which is shared between all requests (which is why you want a static singleton); that’s how it pipelines things to make them really efficient. If the connection dies you want it to automatically recover.

  3. Why do you lazily create ConfigurationOptions (i.e. Lazy)

    Isn’t lazy creation needed only for the connection?
    ie.

    Lazy = conn
    = new Lazy(
    () => ConnectionMultiplexer.Connect( new ConfigurationOptions()
    {

    }
    )

    1. Yes, you can do that. Or even better, use the connection string format instead of ConfigurationOptions and it gets simplified a lot.

  4. public sealed class RedisConnectionSingleton
    {
    private static Lazy lazyConnection = new Lazy(() =>
    {
    return new StackExchangeRedisCacheClient(new NewtonsoftSerializer()
    , “192.168.1.4:6379,syncTimeout=5000,allowAdmin=False,connectTimeout=5000,ssl=False,abortConnect=False”);
    });

    public static StackExchangeRedisCacheClient Instance
    {
    get
    {
    return lazyConnection.Value;
    }
    }

    private RedisConnectionSingleton() { }
    }

  5. I am using same approach but getting too macny timeout error on my webapi when hosted it inside ubuntu. no error when hosted in windows

Leave a Reply

Your email address will not be published. Required fields are marked *