Requesting Data from two Data Stores in Parallel - Cache and Database

Full source code available here

This post is more of a demo of using WhenAny() with different types of Task<> than it is a suggestion or advice on how to use Redis. I advise against simultaneously querying your cache and database for the same data, the purpose of the cache is to take load off the database, not to hit it before checking the cache.

But, by using Redis and MS SQL with Entity Framework I have a simple way of launching two async queries, checking the response of the first, and only allowing the second query to finish if the first does satisfy some condition. I show how to do this when the async tasks are not of the same time, i.e. Task<RedisValue>, and Task<KeyAndValue>.

I’m using a Web Api application for this, and you can find the full source code at the top and bottom of this post, so I’m going to walk through every little step.

Nuget Packages

Add Microsoft.EntityFrameworkCore.SqlServer and StackExchange.Redis to the application.

Startup.cs and Seeding

In Startup.cs, in the ConfigureService(..) method add -

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<KeyAndValueContext>(options =>
    options.UseSqlServer(Configuration["ConnectionStrings:SalesDb"]));
    var multiplexer = ConnectionMultiplexer.Connect("localhost");
    services.AddSingleton<IConnectionMultiplexer>(multiplexer);

These setup the Entity Framework context and Redis multiplexer for dependency injection.

In the Configure(..) method I add KeyAndValueContext keyAndValueContext, IConnectionMultiplexer multiplexer to the method signature, this tells the DI container to inject an Entity Framework context and the Redis multiplexer into the method.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, KeyAndValueContext keyAndValueContext, IConnectionMultiplexer multiplexer)
{
    // snip..

Here I create the database, and seed the Redis cache with 26 key/values, and the database with the same 26 key/values + 26 more that are not in Redis. This way I can be sure that some queries will find results in Redis and database, and some will find results in the database only.

The Controller

In the controller, I add the connection multiplexer and the Entity Framework context to the constructor parameters and assign them to local variables.

private readonly IConnectionMultiplexer _connectionMultiplexer;
private readonly KeyAndValueContext _keyAndValueContext;

public ValuesController(IConnectionMultiplexer connectionMultiplexer, KeyAndValueContext keyAndValueContext)
{
    _connectionMultiplexer = connectionMultiplexer;
    _keyAndValueContext = keyAndValueContext;
}

I have stripped away all methods except for GET. It takes a string as the parameter, setups a cancellation token, and gets a connection to Redis.

[HttpGet("{key}")]
public async Task<IActionResult> Get(string key)
{
    var tokenSource = new CancellationTokenSource();
    var cancellationToken = tokenSource.Token;

    var redis = _connectionMultiplexer.GetDatabase();

Then I fire off the two async requests - one to the database and one to Redis. I use WhenAny to determine when one of the Tasks has completed.

    Task<KeyAndValue> dbTask = _keyAndValueContext.KeyAndValues.Where(kv => kv.Key == key).FirstAsync(cancellationToken);
    Task<RedisValue> redisTask = redis.StringGetAsync(key);

    string value = "";
    var completedTask = await Task.WhenAny(dbTask, redisTask);

The next block is where the fun happens.

I’m using pattern matching to check if the completed task is the Redis one, it almost always will be. Then I check if the value returned is not null, if it is not, I cancel the database request (line 7) and return Ok(value).

However, if the value returned from Redis is null, i.e. the key was not found, I await Task.WhenAny(dbTask) again (line 11). What the dbTask completes I do another bit of pattern matching (line 15), and take the value from the dbTaskCompleted, and finally return Ok(value).

 1    if (completedTask is Task<RedisValue> redisTaskCompleted)
 2    {
 3        value = await redisTaskCompleted;
 4        if (value != null)
 5        {
 6            value = $"key:{key}, value:{value}, source:Redis";
 7            tokenSource.Cancel();
 8        }
 9        else
10        {
11            completedTask = await Task.WhenAny(dbTask);
12        }
13    }
14
15    if (completedTask is Task<KeyAndValue> dbTaskCompleted)
16    {
17        value = $"key:{key}, value:{(await dbTaskCompleted).Value}, source:MSSQL";
18    }
19
20    return Ok(value);

That’s all the source code.

Docker Compose

I am using docker containers to run Redis and MS SQL, and docker compose to run the two.

version: "3.9"
   
services:
  redis:
    image: "redis"
    ports: 
      - "6379:6379"

  mssql:
    image: "mcr.microsoft.com/mssql/server"
    ports: 
      - "1433:1433"
    environment:
    - ACCEPT_EULA=y
    - SA_PASSWORD=A!VeryComplex123Password

Run docker compose up and the will start the two containers.

Testing it out

There is an examples.rest file in the source, with that you can run queries from the REST Client extension for Visual Studio Code.

1GET http://localhost:5000/values/a 
2
3###
4
5GET http://localhost:5000/values/aa

Full source code available here

comments powered by Disqus

Related