Redis Cache as an Alternative to NoSQL

Introduction

 
In most of our projects, we have been using Redis cache mostly as an in-memory cache store to store a few key value pairs. But what if we can extend the capability of this in-memory store to make it work ‘almost’ like a No-SQL. When I say ‘almost’, it's because Redis datastore has its own limitations with regards to writing complex queries on the data. These limitations can also be overcome, however, the overhead would be too much. Let’s discuss this in the following article.
 

Redis Cache and ASP.NET Core framework

 
.NET Core already provides in-built support for caching. You can see the details in this link.
 
However, how and what you can cache will be dependent on the caching store you choose. If you choose Redis cache, there are a variety of options with which you can store values in the cache. The widely used datatypes are as follows:
  • Key,value pairs
  • ReJSON
  • Hash Set

Redis Cache Data Types

 
Key Value pairs ->
 
To store a key value pair, simply enter the name of the key and the corresponding value and set it. Below is the code for doing it:
  1. string strKey1 = "TestKeys1234";  
  2. RedisConnector.Set(strKey1, "1234");  
To retrieve the key, use the following code:
  1. string strResult = RedisConnector.Get<string>(strKey1);  
Here, RedisConnector is the helper class I have written to help connect to the Redis server.
 
ReJSON ->
 
ReJSON is Redis JSON object. It gives the capability of storing your objects as JSON on Redis cache. Here is a sample object,
  1. SampleObject sampleObject = new SampleObject {  
  2.     Country = "India",  
  3.         Id = 7,  
  4.         Name = "Sormita"  
  5. };  
To store this object in Redis cache as a JSON object:

  1. //Saving to Redis using object  
  2. RedisConnector.Set("test1", sampleObject);  
Where “test1” is the name of the key the value for which is object sampleObject.
 
Hash Set->
 
To store an object as HASHSET:
  1. SampleObject sampleObject1 = new SampleObject {  
  2.     Country = "India",  
  3.         Id = 7,  
  4.         Name = "Sormita"  
  5. };  
  6. //Saving a single object to Redis using HashSet  
  7. RedisConnector.HashSet(sampleObject1, "testHash1");  
Where ‘testHash1’ is the name of the key against which hash set object is stored.
 
Looking at the above datatypes, it would be a programmer’s dilemma as to whether to use ReJSON or HASHSET. It depends on the scenario.
 
When using cache memory we should be a miser because ideally in production scenarios, cache memory is limited. When using ReJSON object, the memory used in the cache is more than when using a flat object like HASHSET. How do we know that? Well, in Redis, you can actually see the memory usage.
 
When no objects are stored in the cache and you run the following command, the following shows:
 
Redis Cache As An Alternative To NoSQL
 
Notice the number in the highlighted part.
 
Now, for the above SampleObject class, when the object is stored as ReJSON,
 
Redis Cache As An Alternative To NoSQL
 
When I save the same object as HASHSET, let’s check the memory usage:
 
Redis Cache As An Alternative To NoSQL
 
As you can see, HASHSET is a really good option when it comes to storing objects with the least amount of space. Not only that, but you can also query the object in HASHSET with any of the fields/properties in the object.
 
Below is how you store a list of objects as HASHSET in Redis cache:
  1. //Saving a list to redis using hash set  
  2. var sampleObjects = new RedisList<int, SampleObject>("samples");  
  3. sampleObjects.AddMultiple(lstSampleObject.ToDictionary(x => x.Id));  
Now, if I want to query an object using Country field:
  1. var lstObject = new RedisList<int, SampleObject>("samples");  
  2. var obj2= lstObject.Where(x => x.Value.Country =="Argentina").ToList().FirstOrDefault();  
  3. SampleObject actualObj = obj2.Value;  
I can also update an object at a particular index:
  1. var lstObject = new RedisList<int, SampleObject>("samples");  
  2. SampleObject obj1 = lstObject[2];  
  3. obj1.Name = "New Name";  
  4. lstObject.Remove(2);  
  5. lstObject.Add(2, obj1);  
In the above code, first I am fetching an object from the cached collection of objects ‘samples’. I save this object in obj1. I update the name and add it back to the collection in the cache. I can do this without fetching the entire collection of objects. The Redis datatype used in the above example is HASHSET.
 
The exact scenario where HASHSET will not work proficiently is when we have nested objects.
  1. Company compObject = new Company {  
  2.     CompanyId = 1001,  
  3.         CompanyName = "ABC",  
  4.         UserList = lstUser  
  5. };  
Here, lstUser is a list of User objects. 
 
The serialization and deserialization will not work properly in such cases. To support such scenarios, ReJSON has to be used. To save a complex object like above, we have to first serialize it to proper JSON format. I have used ‘DataContractJsonSerializer’ for this purpose.
  1. public static void SetComplexObject < T > (object obj, string objName) {  
  2.     //Saving and retrieval of a single complex object  
  3.     var ser = new DataContractJsonSerializer(typeof(T));  
  4.     var objJSON = string.Empty;  
  5.     using(var ms = new MemoryStream()) {  
  6.         ser.WriteObject(ms, obj);  
  7.         objJSON = Encoding.UTF8.GetString(ms.ToArray());  
  8.     }  
  9.     RedisConnector.Set(objName, objJSON);  
  10. }  
To deserialize it:
  1. public static T GetComplexObject < T > (string objName) {  
  2.     var ser = new DataContractJsonSerializer(typeof(T));  
  3.     var redisObj = RedisConnector.Get(objName);  
  4.     using(var ms = new MemoryStream(Encoding.UTF8.GetBytes(redisObj.ToString()))) {  
  5.         var processedObj = (T) ser.ReadObject(ms);  
  6.         return processedObj;  
  7.     }  
  8. }  

Conclusion 

 
The above code can be extended to support a list of objects. I have done that and the code for the same can be found in the GitHub link given below. However, the above ReJSONs cannot be queried in the manner, as I have shown above for HASHSET. However, you can attach tags to the objects and query the tag. But that is too much overhead. So as a workaround, you can just flatten your nested objects into flat DTOs (if that is functionally possible), save it as HASHSET, and query and update those easily.
 
So, even for those projects where there is no No-SQL implemented, we have the following scenarios:
  • There is a series of forms (like Wizard) which is updating the same object again and again. In such a case after every form submit instead of doing round trips to the DB server, keep the object in Redis, update as required and finally store the final object at the end of last form submission.
  • There is a dashboard which has a different filter and sort capabilities and visualizations change accordingly. For such a case, get the major chunk of data from the database in one go, keep it in Redis cache and change the filter and sorting and update the view.
The entire code, examples and implementation can be found in the below GitHub link. You can post your questions/suggestions below in the comments section.