Professor Sloth

Free web performance master class

Learn about web performance and how to make your site faster, delivered straight to your inbox.

Episode 14: Storing User Data In Redis

Redis only supports a handful of data types. Our data model has to fit within them. Are we crazy for trying this? It’s time to think about the data model for Request Metrics. We plan to store all customer records in Redis. This requires some thought because Redis is not a relational database. Any linkages between entities like users and customers must be managed by us. The data model should be full enough that we can find the the linkages we need, but not so complex that we continually break the linkages or need complex cleanup operations.

The first thing we need is a user. We’d like to work on authentication soon and we won’t get far without a user to login with. But first, our usage of StackExchange.Redis needs improvement.

Using the StackExchange.Redis Connection Multiplexer

In our first tests of Redis in .NET Core, we played fast and loose to complete an end to end test as quickly as possible. This was at the expense of mis-using the Redis Connection Multiplexer.

The ConnectionMultiplexer must be a singleton that is re-used for the lifetime of your application. It handles disconnects and connection pooling internally. The multiplexer behaves badly if it is re-built for every call so we use a helper to handle the multiplexer for us:


using StackExchange.Redis;
using System;

public class RedisHelper
{
    private static object sync = new object();
    private static ConnectionMultiplexer multiplexer;

    // A simple helper method to get the Redis "client" used for most CRUD operations.
    public static IDatabase GetDatabase()
    {
        return GetMultiplexer().GetDatabase(0);
    }

    public static ConnectionMultiplexer GetMultiplexer()
    {
        // Make sure there is only one instance of the Redis ConnectionMultiplexer
        // for the entire app.
        if (multiplexer == null)
        {
            lock (sync)
            {
                if (multiplexer == null)
                {
                    // Connect to Redis on localhost.
                    // Your connection string will be different.
                    multiplexer = ConnectionMultiplexer
                        .Connect("localhost:6379,allowAdmin=true");
                }
            }
        }

        return multiplexer;
    }
}
RedisHelper.cs

Using Redis Hash Sets

Each user object is JSON encoded into its own field stored in a single “users” Redis HashSet. This has two advantages. First, the total number of redis keys is reduced. Second, the hashset gives an easy way to fetch all users that doesn’t depend on Redis key scans.

Let’s explore basic HashSet CRUD. We use async calls because the StackExchange.Redis authors seem to prefer them:


//
// Our HashSet looks like this conceptually:
//    myHashKey = {
//       myFieldKey: "my value"
//    }
//

// Set a hash key/value
var database = RedisHelper.GetDatabase();
await database.HashSetAsync("myHashKey", "myFieldKey", "my value");

// Get the value of a hash field
//  fieldValue == "my value"
var fieldValue = await database.HashGetAsync("myHashKey", "myFieldKey");

// Get ALL key/value pairs in the hashset
var hashEntries = await database.HashGetAllAsync("myHashKey");

// Delete a single hash key (NOTE: Just the "myFieldKey" field is deleted)
await database.HashDeleteAsync("myHashKey", "myFieldKey")

// Delete an entire hashset
await database.KeyDeleteAsync("myHashKey")


//
// Anything can be stored in a Redis value. We store lots of JSON blobs:
//

// Save an object as JSON to Redis
var myData = new SomeClass();
await database.HashSetAsync("myHashKey", "myFieldKey", JsonSerializer.Serialize(myData));

// Get the object back from Redis
var database = Redis.GetDatabase();
var result = await client.HashGetAsync("myHashKey", "myFieldKey");
if (result != RedisValue.Null)
{
    var myDataFromRedis = JsonSerializer.Deserialize<SomeClass>((string)result);
}
Redis HashSet CRUD Examples

Delete All Redis Data

Early on in development we need to clear the Redis store a lot as we refine the data model. We can do this programatically:


// User our RedisHelper to get the Redis Multiplexer
var multiplexer = RedisHelper.GetMultiplexer();
var server = multiplexer.GetServer(multiplexer.GetEndPoints()[0]);

// Clear ALL data inside the Redis database
server.FlushDatabase(0);
Delete all data in a Redis database

Redis has many more datatypes that we’ll surely use soon. For now, Redis hash sets provide a nice way to group together similar data like our user records. Next time we’ll take our user records and implement simple, cookie based authentication in our ASP.NET Core application.

Jordan Griffin
VP Engineering Request Metrics