What is Azure Cache for Redis?
According to Microsoft, "Azure Cache for Redis provides an in-memory data store based on the open-source software Redis. When used as a cache, Redis improves the performance and scalability of systems that rely heavily on backend data stores. Performance is improved by copying frequently accessed data to fast storage located close to the application. With Azure Cache for Redis, this fast storage is located in-memory instead of being loaded from disk by a database."
One of the ways Insight 2 IMPACT utilized the Redis cache was to to save the output of a logic app that was being called thousands of times a day. By caching the output, we were able to save hundreds of dollars a month by retrieving the output from the cache rather than repeatedly calling the logic app.
In this post, I will show you how to create an Azure Function for getting and setting key/value pairs from the Redis cache.
The full code can be found at https://github.com/davielau/redis-cache-accessor-func.
To run the function locally from Visual Studio, make sure to add a local.settings.json file like the following:
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet",
"redisCacheConnectionString": <Enter redis cache connection string>
}
}
You can get the connection string from the Azure Portal:
The entry point to the function is in RedisCacheAccessorFunc.cs.
public class RedisCacheAccessorFunc
{
private readonly IRedisCacheAccessor _redisCacheAccessor;
public RedisCacheAccessorFunc(IRedisCacheAccessor
redisCacheAccessor)
{
_redisCacheAccessor = redisCacheAccessor;
}
/// <summary>
/// PUT request will add to the redis cache. GET request will
/// retrieve from the redis cache.
/// </summary>
/// <param name="req"></param>
/// <param name="log"></param>
/// <returns></returns>
[FunctionName("GetPutRedisCache")]
public IActionResult Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "put",
Route = null)] HttpRequest req, ILogger log)
{
var reqHeaders = req.Headers;
string cacheKey = reqHeaders["cacheKey"];|
string cacheValue = reqHeaders["cacheValue"];
string cacheConnectionString = Environment
.GetEnvironmentVariable("redisCacheConnectionString");
try
{
string retValue =
_redisCacheAccessor.ReadWriteFromCache
(cacheConnectionString, req.Method, cacheKey, cacheValue);
return new OkObjectResult(retValue);
}
catch(Exception e)
{
log.LogError(e.Message, e);
throw;
}
}
}
As you can see, this class is very simple. All it does is read is read the cache key and value from the request header and the connection string. It then passes these values to the _redisCacheAccessor.ReadWriteFromCache method. I try to keep the entry point as simple as possible and put the bulk of my logic in a separate project. In my example, that is the i2i.redisaccessor.core project.
This will minimize any rework if Microsoft decides to change the Run method signature in the future as they did when they went from v1 to v2 functions. This will also make your code more portable if one day you decide to port your functions to somewhere else like AWS.
So you may be wondering what's with the use of the IRedisCacheAccessor interface and the RedisCacheAccessorFunc property at the top of the class:
private readonly IRedisCacheAccessor _redisCacheAccessor; public RedisCacheAccessorFunc(IRedisCacheAccessor
redisCacheAccessor)
{
_redisCacheAccessor = redisCacheAccessor;
}
This is for Dependency Injection (DI). Basically, the implementation of _redisCacheAccessor is "injected" during runtime. This aids in unit testing and promotes a loosely coupled architecture. The implementation is injected in the Startup.cs class.
[assembly: FunctionsStartup(
typeof(i2i.redisaccessor.function.Startup))]
namespace i2i.redisaccessor.function
{
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddLogging();
builder.Services.AddHttpClient();
builder.Services.AddSingleton
<IRedisCacheAccessor, RedisCacheAccessor>();
builder.Services.AddSingleton<ICacheConnector,
CacheConnector>();
}
}
}
You can see that the RedisCacheAccessor class is being used as the implementation for the IRedisCacheAccessor interface.
Now let's take a look at the core logic inside RedisCacheAccessor.cs.
public string ReadWriteFromCache(string cacheConnectionString,
string reqMethod, string cacheKey, string cacheValue)
{
IDatabase dbCache = _cacheConnector
.getDbCache(cacheConnectionString);
if (reqMethod == System.Net.WebRequestMethods.Http.Put)
{
dbCache.StringSet(cacheKey, cacheValue,
new TimeSpan(24, 0, 0));
log.LogInformation(String.Format("PUT cacheKey={0}
cacheValue={1} ", cacheKey, cacheValue));
return cacheKey;
}
else if (reqMethod == System.Net.WebRequestMethods.Http.Get)
{
string cacheReturn = (string)dbCache.StringGet(cacheKey)
?? "Unknown";
log.LogInformation(String.Format("GET cacheKey={0}
cacheValue={1} ", cacheKey, cacheReturn));
return cacheReturn;
}
else
{
throw new Exception(reqMethod + "method not allowed");
}
}
If the web request method is PUT, then we add the key/value pair to the cache. If the web request method is GET, then we retrieve the value based on the key. Let's take a closer look at the implementation of _cacheConnector, which is retrieving the cache instance for us. This is located in CacheConnector.cs.
public class CacheConnector: ICacheConnector
{
/// <summary>
/// Static dictionary to hold all redis connections.
/// Dictionary key is the redis cache connection string
/// </summary>
private static
Dictionary<string, ConnectionMultiplexer> redisConnections;
public IDatabase getDbCache(string cacheConnectionString)
{
//instantiate dictionary if null
if (redisConnections == null)
{
redisConnections = new Dictionary
<string, ConnectionMultiplexer>();
}
//Retrieve the connection from the dictionary. If it doesn't
//exist, add the connection to the dictionary
ConnectionMultiplexer connection;
if (!redisConnections.TryGetValue
(cacheConnectionString, out connection))
{
connection = ConnectionMultiplexer
.Connect(cacheConnectionString);
redisConnections.Add(cacheConnectionString, connection);
}
IDatabase dbCache = connection.GetDatabase();
return dbCache;
}
}
I've coded this class so it can be used for multiple different redis caches. Based on the connection string, I store the connection inside the static redisConnections member variable. Instead of instantiating a new connection each time, I just retrieve the connection based on the connection string.
To test this function, we can deploy to Azure by right clicking the function and choosing publish.
Or we can just hit the Play button in Visual Studio to test locally.
Let's now test this function using Postman. We will send a PUT request with the key of "colour" and the value of "red". We get a successful status 200 response.
We can send a GET request to retrieve the value for the "colour" key. We can see that it returns red.
What happens if we try to retrieve a key that doesn't exist. Let's try a GET request with the key of "tint". We get "Unknown" as a response.
I hope you found this post useful. Thanks for reading!