So let's say that you want to maintain a list of unique items, with items sorted based on the last access time. Few examples of such use case - List of recent actions to be shown against a user profile, List of items in a purchase cart, List of my favorite dishes in a catalog etc.
While there are multiple technology options that can help in this scenario, i wanted to highlight the usage of Redis store for such cases. Redis supports multiple data structures, each serving a set of unique scenario. SortedSet is one such data structure that helps in this particular scenario.
From Redis.IO
Sorted sets are a data type which is similar to a mix between a Set and a Hash. Like sets, sorted sets are composed of unique, non-repeating string elements, so in some sense a sorted set is a set as well.
While there are multiple technology options that can help in this scenario, i wanted to highlight the usage of Redis store for such cases. Redis supports multiple data structures, each serving a set of unique scenario. SortedSet is one such data structure that helps in this particular scenario.
From Redis.IO
Sorted sets are a data type which is similar to a mix between a Set and a Hash. Like sets, sorted sets are composed of unique, non-repeating string elements, so in some sense a sorted set is a set as well.
However while elements inside sets are not ordered, every element in a sorted set is associated with a floating point value, called the score (this is why the type is also similar to a hash, since every element is mapped to a value).
Moreover, elements in a sorted sets are taken in order (so they are not ordered on request, order is a peculiarity of the data structure used to represent sorted sets). They are ordered according to the following rule:
- If A and B are two elements with a different score, then A > B if A.score is > B.score.
- If A and B have exactly the same score, then A > B if the A string is lexicographically greater than the B string. A and B strings can't be equal since sorted sets only have unique elements.
I hope things are clear now. Let us get on with a test application code in C# and .NET core.
Let us make an Item class.
public class Item
{
public string Id { get; set; }
public string Name { get; set; }
public string Format()
{
return $"{Id}:{Name}";
}
}
Add a reference to StackExchange.Redis nuget package.
var connection = await ConnectionMultiplexer.ConnectAsync("");
var database = connection.GetDatabase();
string setName = "";
Stopwatch sw = new Stopwatch();
// First insert 20 unique items
sw.Restart();
for (int i = 0; i < 20; i++)
{
Item item = new Item()
{
Id = "90ddc267-e96c-4d48-b05e-43c3cf1e5de1",
Name = "FixedName" + i.ToString()
};
Stopwatch sw1 = new Stopwatch();
sw1.Start();
await database.SortedSetAddAsync(setName, item.Format(), DateTime.UtcNow.Ticks);
sw1.Stop();
Console.WriteLine($"Time taken for 1 SortedSetAddAsync {sw1.ElapsedMilliseconds}");
}
sw.Stop();
Console.WriteLine($"Time taken for 20 SortedSetAddAsync {sw.ElapsedMilliseconds}");
// Try getting top 10 items with their score i.e. access time
var items = await database.SortedSetRangeByRankWithScoresAsync(setName, order: Order.Descending, stop: 9);
Console.WriteLine(items);
// Try overriding items in reverse order
sw.Restart();
for (int i = 19; i >= 0; i--)
{
Item item = new Item()
{
Id = "90ddc267-e96c-4d48-b05e-43c3cf1e5de1",
Name = "FixedName" + i.ToString()
};
Stopwatch sw1 = new Stopwatch();
sw1.Start();
await database.SortedSetAddAsync(setName, item.Format(), DateTime.UtcNow.Ticks);
sw1.Stop();
Console.WriteLine($"Time taken for 1 SortedSetAddAsync {sw1.ElapsedMilliseconds}");
}
sw.Stop();
Console.WriteLine($"Time taken for 20 SortedSetAddAsync {sw.ElapsedMilliseconds}");
// Try getting top 10 items with their score i.e. access time
items = await database.SortedSetRangeByRankWithScoresAsync(setName, order: Order.Descending, stop: 9);
sw.Stop();
Console.WriteLine($"Time taken for top 10 SortedSetRangeByRankWithScoresAsync {sw.ElapsedMilliseconds}");
Console.WriteLine(items);
// Remove items that are not part of top 10 items list
sw.Restart();
await database.SortedSetRemoveRangeByRankAsync(setName, 10, -1);
sw.Stop();
Console.WriteLine($"Time taken for SortedSetRemoveRangeByRankAsync {sw.ElapsedMilliseconds}");
// verify that indeed items have been removed
sw.Restart();
items = await database.SortedSetRangeByRankWithScoresAsync(setName, order: Order.Descending, stop: 9);
sw.Stop();
Console.WriteLine($"Time taken for top 10 SortedSetRangeByRankWithScoresAsync {sw.ElapsedMilliseconds}");
Console.WriteLine(items);
Once you run it, you will realize 2 things.
1. This data structure is quite powerful and handles a lot of concurrency related scenarios out of the box. Less work for you.
2. The operations are fast!! (provided you do not have any major network lag.)