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):
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.