From Redis to KeyDB with Nginx as load balancer and .Net Core

Part 1 - Using Redis Docker image with .Net Core API application

Ivaylo Gluhchev
8 min readMay 10, 2021

With my team we decided that we need a more robust approach to the implementation of our caching solution. Currently we are using a single Redis instance for caching, which is fine for part of our application. We wanted to move from a single instance of caching database to a cluster with at least two instances. We decided to research what alternatives there are and is there something that could fit more within our needs. We decided to look into KeyDB and what it could offer us.

This will be a short series of articles describing what the research led to. Before continuing further let me first share what this series would be and what it would not be.

It would be a quick walkthrough a simple setup of an ASP.NET Core web API application that is going to use a single Redis instance for caching and we are going to replace it with two instances of KeyDB running in Active Replica setup behind a Nginx load balancer. Along the way I will share a hint or two that you might find useful.

I will not bore you with too much details about what each component is, but I do think that a brief description should be present. Feel free to skip any part of this article and go to whatever part feels right for you.

What is Redis and what is KeyDB

For those of you who are not that familiar with Redis, it is a very popular maybe the most popular in-memory, key-value store. Redis is also open source. It is usually used for cache, message broker or as a database. You could find detailed information here.

Then what is KeyDB - it’s Redis on steroids (wish that phrase was mine as I like the sound of it but must admit that it’s not). KeyDB is a fork of Redis but with some tweaks and a strong focus on multithreading, high throughput and of course memory efficiency. Being Redis’s fork makes it a real good and easy to switch alternative which is compatible with Redis API, clients, protocols, modules etc. You can go to KeyDB dev portal for more details or wander around their documentations .

After this brief introduction we are going to start our first part.

C# Web Api app with Redis caching

We will start with the very basic out of the box web api application in .net core 3.1 using our favorite weather app which will use Redis for caching. We are going to tweak it a little bit in order to serve our needs.

First we will add the following block to appsettings.json to have the Redis server url in the connection strings and to allow all hosts to able to access our resources for simplicity. Of course in production you should have more control on that.

{
...
,
"AllowedHosts": "*",
"ConnectionStrings": {
"redisServerUrl": "localhost:46379"
}
}

Then we will install the Microsoft.Extensions.Caching.StackExchangeRedis Nuget package from Package manager or we can run in the console Install-Package Microsoft.Extensions.Caching.StackExchangeRedis -Version 5.0.1 . We can skip the -Version 5.0.1 part if we do not want to install a specific version.

After we have successfully installed the package, next we will modify the Startup.cs file as follows:

public void ConfigureServices(IServiceCollection services){
...
// Redis
services.AddStackExchangeRedisCache(options =>
{
options.Configuration =
this.Configuration.GetConnectionString("redisServerUrl");
});
}

Before changing any further our demo app let’s go to docker and run Redis image in a container. For the command that I will execute and for all the other similar commands I will use PowerShell with elevated admin rights. The command is

docker run --name <my-container-name> -p <my-chosen-port>:6379 -d redis

It will run the container forwarded on port 6379 in detached mode (the image will be automatically downloaded if it does not exist locally).

docker run --name redis-container -p 46379:6379 -d redis

running redis in docker

I have chosen port 46379 as it’s easy for me to remember as it is close the default Redis port 6379 and the name of the container is redis-container. We can verify that we can interact with our redis-container by starting the redis-cli command interface.

Now we will execute a command in the running container with the option -it which will basically make the container look like a terminal connection session.

docker exec -it <container_name> /bin/bash

The following command will start the cli itself.

redis-cli

Once the cli is started we can add, get and delete data from there with some very simple and easy to use commands.

Now that we know we can interact with the cli and Redis let’s get back to our application and add a new class holding a couple of extension methods that will make it easier to work with our cache.

using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;
using System.Threading.Tasks;
namespace RedisVsKeyDb.Extensions
{
public static class CacheExtensions
{
public static async Task<T> GetCacheValueByKeyAsync<T>(this IDistributedCache cache, string key) where T : class
{
string result = await cache.GetStringAsync(key);
if (string.IsNullOrEmpty(result))
{
return null;
}
var deserializedResult = JsonSerializer.Deserialize<T>(result);
return deserializedResult;
}
public static async Task SetCacheValueAsync<T>(this IDistributedCache cache, string key, T value) where T : class
{
DistributedCacheEntryOptions cacheEntryOptions = new DistributedCacheEntryOptions();
// Remove item from cache
// cacheEntryOptions.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(60);
// Remove item from cache if unused for the duration
// cacheEntryOptions.SlidingExpiration = TimeSpan.FromSeconds(30);
string result = JsonSerializer.Serialize(value);
await cache.SetStringAsync(key, result.ToString(), cacheEntryOptions);
}
}
}

If we want to set expiration to the cache we can uncomment the lines for cacheEntryOptions.SlidingExpiration = TimeSpan.FromSeconds(30); and cacheEntryOptions.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(60);. After adding these cache extensions we will move to our Weather controller and make use of them, but before that lets extend our WeatherForecast.cs model with a property that will clearly indicate if the data is coming from the cache or not.

public class WeatherForecast
{
...
public bool Cached { get; set; }
}

And now to the controller.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using RedisVsKeyDb.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace RedisVsKeyDb.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
private readonly IDistributedCache _cache;
public WeatherForecastController(ILogger<WeatherForecastController> logger, IDistributedCache cache)
{
_logger = logger;
_cache = cache;
}

public async Task<IEnumerable<WeatherForecast>> Get()
{
// Check if content exists in cache
var weatherResult = await _cache.GetCacheValueByKeyAsync<WeatherForecast[]>("weather-forecast");
if (weatherResult != null)
{
foreach (var item in weatherResult)
{
item.Cached = true;
}
return weatherResult;
}
var rng = new Random();
weatherResult = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
}).ToArray();

// Add to cache
await _cache.SetCacheValueAsync("weather-forecast", weatherResult);
return weatherResult;
}
}
}

I know the example itself does not make much sense in the real world but it definitely serves our purposes. And yes I know that all the stuff happening in the controller does not belong there but I do not want to complicate things at this point.

Before running our application I want to check if we have any data in our Redis instance. In order to that I will use the command KEYS * and hit enter. What that will do is to get all the keys we have stored in the db. We will see that there is nothing yet.

After that we will add a new key value pair with the SET command which has the following format. SET <key> <value> Then after we repeat KEYS * we will see that we have a key - "my-first-key"

We know now that in Redis there is one key-value entry which we have added manually. Next we will start the application.

When our Action method of the WeatherForecastController gets called it checks if the data that the client is trying to retrieve exists in the cache. If it does not exist the app will continue to make the magical weather calculations and the code that we have added will add the new weather data to the cache. The result shows that ‘cached’ is false because the data was not retrieved from the cache.

“cached” is false because the data does not come from the cache

Let’s call the endpoint once again and see where the data comes from.

“cached” is true because the data comes from the cache

We clearly see that the value of ‘cached’ is true. With the first call we added the data to the cache because it was not there. With the second call the data was retrieved from the cache.

And now let’s check what is going on in our redis-cli once again.

After running our application we see that there is also another key "weather-forecast" which is there because we have added it through the code. To see the value behind our key we will type: GET "weather-forecast" . But we get an error. This is because the value behind this key is not a simple string. There are several different commands to retrieve the data by key depending on the data types. If you are not sure what the datatype is you can always check with TYPE "weather-forecast". That will show you what command you should use depending on the datatype. In our case the type is HASH so we will use HGETALL "weather-forecast" And we see that our interaction with Redis through our application was successful and the data is cached and retrieved from there.

Adding cache to a .NET Core application is as simple as that, of course there is a lot more than this like cache expiration time, cache invalidation and so on but what we have here is a good start.

Part 2 Switching from single Redis instance to 2 KeyDB instances running in an active replica mode.

--

--

No responses yet