Redis: RedisString and RedisJSON

Introduction

Redis is a fast, in-memory, key-value data store used widely for caching, session storage, and real-time data processing. While Redis traditionally handles data as simple strings (with structures like lists, sets, and hashes), it has evolved to support more complex data types, especially JSON, which is often the format of choice for modern applications dealing with complex nested data structures.

This article will explore two ways to handle JSON data in Redis using RedisString and RedisJSON. We will cover the fundamental differences between these approaches, why RedisJSON is preferable for large JSON objects, and provide real-world use cases to highlight when and why you should consider RedisJSON. The purpose is to measure the performance, data handling capabilities, and potential use cases for each approach.

What is RedisString?

RedisString is the most basic and commonly used data type in Redis. It stores data as a simple string value, allowing for GET, SET, and more operations. You can store JSON data inside a RedisString by serializing the JSON object into a string format.

What is RedisJSON?

RedisJSON is a Redis module that provides native support for storing, querying, and manipulating JSON data inside Redis. It allows you to store JSON documents and perform fine-grained operations on specific fields without needing to retrieve the entire document. RedisJSON is purpose-built to handle large JSON data efficiently and support complex JSON structures like nested objects, arrays, and primitives.

Now, let us understand everything with a POC.

Table of Contents

  1. Project Setup
  2. Prerequisites
  3. Running the ASP.NET Core Web API Project
  4. Available Endpoints
  5. Analyzing Performance Results
  6. Conclusion

Project Setup

Please find the attachment zip file with the following.

  • Source Code: The source code has both RedisString and RedisJSON APIs available.
  • Test_Result_Images: The performance comparison, including key metrics (latency and data received).
  • JSON File: JSON file to test code on your local machine.
  • Instructions to Run the Project: Txt file with instructions to run the project.

Prerequisites

  • Redis Server installed and running (version x.x.x)
  • .NET Core SDK installed

ASP.NET Core Web API Project

You will need the following dependencies in your ASP.NET Core Web API project.

  • StackExchange.Redis for RedisString operations.
  • NRedisStack for RedisJSON operations.
  • Run the project

Available Endpoints

Your Web API exposes several endpoints to perform different operations with both RedisString and RedisJSON. Here are the key endpoints and how to use them.

  1. Upload JSON file: We will first upload a JSON file and then perform all the operations to see the difference.
    • RedisString: POST /api/redisString/upload-file
    • RedisJSON: POST /api/redisJson/upload-file
  2. Clean Cache: This endpoint is used to clean the cache.
    • RedisString: POST /api/redisString/CleanCache
    • RedisJSON: POST /api/redisJson/CleanCache
  3. get-data: This endpoint is used to get all the JSON data.
    • RedisString: POST /api/redisString/get-data
    • RedisJSON: POST /api/redisJson/get-data
  4. get-dataById: This endpoint is used to get data by id.
    • RedisString: POST /api/redisString/get-dataById
    • RedisJSON: POST /api/redisJson/get-dataById
  5. check-KeyExists: This endpoint is used to check if the Redis key exists or not.
    • RedisString: POST /api/redisString/check-KeyExists
    • RedisJSON: POST /api/redisJson/check-KeyExists

Analyzing Performance Results

First, upload the JSON file using the upload-file endpoint. This endpoint will upload the file to the project directory and save the data into the cache.

Using RedisString

[HttpPost("upload-file")]
public async Task<IActionResult> UploadJson([FromForm] IFormFile jsonFile)
{
    if (jsonFile == null || jsonFile.Length == 0)
    {
        return BadRequest("Invalid file or no file was uploaded.");
    }

    var fileExtension = Path.GetExtension(jsonFile.FileName);
    if (fileExtension.ToLower() != ".json")
    {
        return BadRequest("Only JSON files are allowed.");
    }

    if (jsonFile.ContentType != "application/json")
    {
        return BadRequest("File content type is not valid. Only JSON files are allowed.");
    }

    try
    {
        var jsonFiles = Directory.GetFiles(_uploadDirectory, "*.json");
        foreach (var file in jsonFiles)
        {
            System.IO.File.Delete(file);
        }

        // Save the file to the project directory
        var filePath = Path.Combine(_uploadDirectory, jsonFile.FileName);
        using (var fileStream = new FileStream(filePath, FileMode.Create))
        {
            await jsonFile.CopyToAsync(fileStream);
        }

        // Read the content from the file and cache it using Redis
        var jsonData = await System.IO.File.ReadAllTextAsync(filePath);

        // Deserialize the JSON to validate its format
        var data = JsonSerializer.Deserialize<object>(jsonData);

        // Get the file size (in MB)
        var fileSizeKB = jsonFile.Length / (1024.0 * 1024.0);
        await _redisString.UploadFile();

        return Ok(new
        {
            Message = "File uploaded successfully",
            FileSizeMB = fileSizeKB
        });
    }
    catch (JsonException)
    {
        return BadRequest("Invalid JSON format.");
    }
    catch (Exception)
    {
        return StatusCode(500, "Internal server error.");
    }
}
public async Task UploadFile()
{
    // Remove Redis cache
    await _cache.RemoveAsync(cacheKey);

    // Cache miss, read from JSON file
    var data = await ReadJsonFileAsync();
    if (data.ActionAccessRight.Count > 0)
    {
        // Save the data into RedisString
        await _cache.SetStringAsync(cacheKey, 
            JsonSerializer.Serialize(data.ActionAccessRight));
    }
}

In the above two methods, we have uploaded the JSON file into the project directory and saved the data into Redis using redisString.

Output

JSON file

Using RedisJSON

[HttpPost("upload-file")]
public async Task<IActionResult> UploadJson([FromForm] IFormFile jsonFile)
{
    if (jsonFile == null || jsonFile.Length == 0)
    {
        return BadRequest("Invalid file or no file was uploaded.");
    }

    var fileExtension = Path.GetExtension(jsonFile.FileName);
    if (fileExtension.ToLower() != ".json")
    {
        return BadRequest("Only JSON files are allowed.");
    }

    if (jsonFile.ContentType != "application/json")
    {
        return BadRequest("File content type is not valid. Only JSON files are allowed.");
    }

    try
    {
        var jsonFiles = Directory.GetFiles(_uploadDirectory, "*.json");
        foreach (var file in jsonFiles)
        {
            System.IO.File.Delete(file);
        }

        // Save the file to the project directory
        var filePath = Path.Combine(_uploadDirectory, jsonFile.FileName);
        using (var fileStream = new FileStream(filePath, FileMode.Create))
        {
            await jsonFile.CopyToAsync(fileStream);
        }

        // Read the content from the file and cache it using Redis
        var jsonData = await System.IO.File.ReadAllTextAsync(filePath);

        // Deserialize the JSON to validate its format
        var data = JsonSerializer.Deserialize<object>(jsonData);

        // Get the file size (in MB)
        var fileSizeKB = jsonFile.Length / (1024.0 * 1024.0);
        await _accessRepository.UploadFile();

        return Ok(new
        {
            Message = "File uploaded successfully",
            FileSizeMB = fileSizeKB
        });
    }
    catch (JsonException)
    {
        return BadRequest("Invalid JSON format.");
    }
    catch (Exception)
    {
        return StatusCode(500, "Internal server error.");
    }
}
public async Task UploadFile()
{
    // Remove Redis JSON
    var res = await _redisDb.JSON().ForgetAsync(cacheKey, path: "$");

    // Cache miss, read from JSON file
    var data = await ReadJsonFileAsync();

    if (data.ActionAccessRight.Count > 0)
    {
        // Save the data into RedisJSON using root path
        _redisDb.JSON().Set(cacheKey, "$", data, When.Always);
    }
}

In the above two methods, we have uploaded the file into the project directory and saved the data into redisJSON using the root path.

Output

Project directory

Now, we will use two endpoints, get-dataById, and check-keyExists, to check the performance in both cases.

get-dataById

Using RedisString

public async Task<(List<ActionAccessRight> AccessRights, TimeSpan Latency, int DataSize)> GetAccessRightByIdAsync(long menuId, long accessRightId)
{
    List<ActionAccessRight> accessRights;
    TimeSpan redisLatency = new TimeSpan();
    var stopwatch = new Stopwatch();
    stopwatch.Start();

    // Get data from Redis
    var result = await _cache.GetStringAsync(cacheKey);

    var res = JsonSerializer.Deserialize<List<ActionAccessRight>>(result.ToString());

    // Get the data for menuId and accessRightId using LINQ
    accessRights = res.Where(a => a.MenuId == menuId && a.AccessRightId == accessRightId).ToList();

    stopwatch.Stop();
    redisLatency = stopwatch.Elapsed;

    // Data size processed
    int dataSize = (int)ConvertBytesToKB((result?.ToString()?.Length ?? 0) * sizeof(char));

    return (accessRights, redisLatency, dataSize);
}

Output

Output

As you can see, the latencyinmilliseconds and dataReceivedFromRedisInKb. Now, we will check in the case of RedisJson.

Using RedisJSON

public async Task<(List<ActionAccessRight> AccessRights, TimeSpan Latency, int DataSize)> GetAccessRightByIdAsync(long menuId, long accessRightId)
{
    List<ActionAccessRight> accessRights = null;
    var stopwatch = new Stopwatch();
    TimeSpan redisLatency = new TimeSpan();
    var jsonPath = $"$..[?(@.MenuId=={menuId} && @.AccessRightId=={accessRightId})]";
    
    stopwatch.Start();
    var result = await _redisDb.JSON().GetAsync(cacheKey, path: jsonPath);
    stopwatch.Stop();
    redisLatency = stopwatch.Elapsed;

    // Data size processed
    int dataSize = (int)ConvertBytesToKB((result?.ToString()?.Length ?? 0) * sizeof(char));

    if (!result.IsNull)
    {
        accessRights = JsonSerializer.Deserialize<List<ActionAccessRight>>(result.ToString());
    }

    return (accessRights, redisLatency, dataSize);
}

Output

Params

Now, we will see all the metrics using the table given below.

  RedisString RedisJson
Latency(ms) 472 305
Data Received(Kb) 37196 50

Now we will move to another endpoint check-Keyexists.

check-KeyExists

Using RedisString

public async Task<(bool AccessRights, TimeSpan Latency, int DataSize)> CheckAccessRightByIdAsync(long menuId, long accessRightId)
{
    List<ActionAccessRight> accessRights = null;
    var stopwatch = new Stopwatch();
    TimeSpan redisLatency = new TimeSpan();

    try
    {
        stopwatch.Start();
        var result = await _cache.GetStringAsync(cacheKey);
        var res = JsonSerializer.Deserialize<List<ActionAccessRight>>(result.ToString());

        accessRights = res.Where(a => a.MenuId == menuId && a.AccessRightId == accessRightId).ToList();
        stopwatch.Stop();
        redisLatency = stopwatch.Elapsed;

        // Data size processed
        int dataSize = (int)ConvertBytesToKB((result?.ToString()?.Length ?? 0) * sizeof(char));

        if (accessRights.Count == 0)
        {
            return (false, redisLatency, dataSize);
        }

        return (true, redisLatency, dataSize);
    }
    catch (Exception)
    {
        return (false, redisLatency, 0);
    }
}

Output

Check-KeyExists

As you can see, the latencyInMilliseconds and dataReceivedFromRedisInKb. Now, we will check in the case of RedisJson.

Using RedisJSON

public async Task<(bool AccessRights, TimeSpan Latency, int DataSize)> CheckAccessRightByIdAsync(long menuId, long accessRightId)
{
    List<ActionAccessRight> accessRights = null;
    var stopwatch = new Stopwatch();
    TimeSpan redisLatency = new TimeSpan();

    try
    {
        var jsonPath = $"$..[?(@.MenuId=={menuId} && @.AccessRightId=={accessRightId})]";
        stopwatch.Start();
        var result = await _redisDb.JSON().TypeAsync(cacheKey, path: jsonPath);
        stopwatch.Stop();
        redisLatency = stopwatch.Elapsed;

        // Data size processed
        int dataSize = (int)ConvertBytesToKB((result?.ToString()?.Length ?? 0) * sizeof(char));

        if (result.Length == 0)
        {
            return (false, redisLatency, dataSize);
        }

        return (true, redisLatency, dataSize);
    }
    catch (Exception)
    {
        return (false, redisLatency, 0);
    }
}

Output

RedisJSON

Now, we will see all the metrics using the table given below.

  RedisString RedisJson
Latency(ms) 487 393
Data Received(Kb) 37196 0


Conclusion

By running this ASP.NET Core Web API project, you can clearly see the performance advantages of RedisJSON over RedisString, especially when dealing with large or complex JSON data. RedisString is simple to use but quickly becomes inefficient for large-scale JSON handling due to its lack of partial updates, higher memory consumption, and increased network overhead. RedisJSON, with its ability to update and retrieve parts of a JSON document, provides a much more efficient solution.

Key Takeaways

  • RedisString is suitable for simple use cases but lacks efficiency when handling large or frequently updated JSON objects.
  • RedisJSON offers partial updates, more efficient memory usage, atomic operations, and faster queries, making it ideal for modern applications dealing with complex, nested data structures.

By understanding and analyzing the performance of both RedisString and RedisJSON through this project, you can make more informed decisions about how to store and manage JSON data in Redis, depending on your specific application needs.

Thank You, and Stay Tuned for More!