Note: phiên bản Tiếng Việt của bài này ở link dưới.
https://duongnt.com/stackexchangeredis-vie
This is the second part of a three-part series about Redis-based distributed caching. You can find the other parts from here.
Among .NET Platform Extensions, we have an interface called IDistributedCache. This interface has methods to provide distributed caching to serialized values. To use this interface with Redis as the caching backend, we can use the Microsoft.Extensions.Caching.StackExchangeRedis package. You can install it with this command.
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
Note that this package is different from the StackExchange.Redis package.
Set up a connection
First, we need a Redis instance to run all the test code in this article. Please see the first part for how to set up Redis inside a Docker container.
With StackExchangeRedis, integrating with Redis is very simple. We specify the Redis instance with this configuration object.
var configurationOptions = new ConfigurationOptions
{
EndPoints = { "localhost:6379" }, // Unlike aioredis, we don't need to specify "redis://" here
Ssl = false // Set this to true if your Redis instance can handle connection using SSL
};
ConfigurationOptions
also has other settings, below are some of the more important ones in my opinion.
Password
: a password to authenticate with Redis.ConnectTimeout
: the maximum time to wait to establish a connection to Redis.AsyncTimout
: the maximum time allows for an async operation.SyncTimeout
: the maximum time allows for a sync operation.
Personally, I think we should always set ConnectTimeout
and AsyncTimout/SyncTimeout
to a low value. Because if we have trouble connecting to Redis then the default, which is 5,000 milliseconds, can seriously clog up our processing pipeline. And a cache is supposed to be fast anyway.
After that, we can add the Redis cache service to our service collection and request an IDistributedCache
instance like this.
var serviceProvider = new ServiceCollection()
.AddStackExchangeRedisCache(options => options.ConfigurationOptions = configurationOptions)
.BuildServiceProvider();
var cache = serviceProvider.GetRequiredService<IDistributedCache>();
Then we can use the cache
object to communicate with the Redis server.
Write data into Redis with StackExchangeRedis
We should keep in mind that StackExchangeRedis is not a Redis client for C# .NET. Instead, it is just an implementation of IDistributedCache
using Redis as the caching backend. Because of that, it does not support all the data types and features of Redis. It only supports reading and writing byte arrays. And even though it supports synchronous operation with the Set
method, we only focus on asynchronous operation with SetAsync
method in this article.
To write a byte array into Redis, we use the SetAsync
method like this.
var token = <code to create a cancellation token> // can omit this if you don't need to cancel the request
cacheOptions = new DistributedCacheEntryOptions
{
AbsoluteExpiration = new DateTimeOffset(new DateTime(2089, 10, 17, 07, 00, 00), TimeSpan.Zero),
SlidingExpiration = TimeSpan.FromSeconds(3600)
})
await cache.SetAsync("string", Encoding.UTF8.GetBytes("Example"), cacheOptions, token);
The meaning of each cache entry option is below.
AbsoluteExpiration
: the time when this cache entry will expire. This is aDateTimeOffset
, which is aDateTime
value and an offset compared to UTC.SlidingExpiration
: if this entry is inactive (no read/write access) for longer than this value, it will be removed. Note thatSlidingExpiration
cannot extend the life of an entry beyondAbsoluteExpiration
.
Note: if by some miracle you are reading this article after 2089/10/17 07:00:00 UTC
then please set the AbsoluteExpiration
to a further time. Because if AbsoluteExpiration
was in the past then an exception will be thrown. I highly doubt that Redis or my blog would still be a thing in 2089 though.
What if we don’t want our entry to expire?
Inside Microsoft.Extensions.Caching.Abstractions
namespace, we have some extension methods to make using IDistributedCache
easier. If we don’t care about cache expiration then we can call this method.
await cache.SetAsync("string_2", Encoding.UTF8.GetBytes("Example"), token);
The extension method will automatically create and use a default DistributedCacheEntryOptions
. This means our cache entry won’t ever expire because both expiration will be set to -1
as we can see here.
private const long NotPresent = -1;
// bunch of code
await cache.ScriptEvaluateAsync(SetScript, new RedisKey[] { _instance + key },
new RedisValue[]
{
absoluteExpiration?.Ticks ?? NotPresent,
options.SlidingExpiration?.Ticks ?? NotPresent,
GetExpirationInSeconds(creationTime, absoluteExpiration, options) ?? NotPresent,
value
});
What if we only need to write strings?
If we only need to write strings into Redis then manually converting a string to byte array every time can get tiring. Fortunately, we have an extension method that allows us to write this.
await cache.SetStringAsync("string_3", "Example", cacheOptions, token);
A quick look at the implementation here reveals that this method simply encodes the string using UTF-8 and calls SetAsync
with the encoded string.
return cache.SetAsync(key, Encoding.UTF8.GetBytes(value), options, token);
There is also an extension method to handle the case where we want to write a non-expiring string.
await cache.SetStringAsync("string_4", "Example", token);
How does Redis store our data?
Looking at the code of StackExchangeRedis, we can see that it uses the HMSET
command to save data into Redis. From this, we can guess that our data is stored as hash.
private const string SetScript = (@"
redis.call('HMSET', KEYS[1], 'absexp', ARGV[1], 'sldexp', ARGV[2], 'data', ARGV[4])
if ARGV[3] ~= '-1' then
redis.call('EXPIRE', KEYS[1], ARGV[3])
end
return 1");
We can also check this with redis-cli.
127.0.0.1:6379> type string
hash
And we can also check all fields in the hash.
127.0.0.1:6379> hgetall string
1) "absexp"
2) "659159676000000000"
3) "sldexp"
4) "36000000000"
5) "data"
6) "Example"
We can see that the data itself is stored in the data
field. And the two expirations are stored as absexp
and sldexp
, their values are a little more complicated though.
absexp
: this is the difference between the absolute expiration time and0001/01/01 00:00:00 UTC
, in ticks. One second is10,000,000
ticks.sldexp
: this is the sliding expiration, also converted into ticks.
Read data from Redis with StackExchangeRedis
Just like when writing data into Redis, we only focus on asynchronous operation with the GetAsync
method.
var token = <code to create a cancellation token> // can omit this if you don't need to cancel the request
var stringBytes = await cache.GetAsync("string", token);
Console.WriteLine(Encoding.UTF8.GetString(stringBytes)); // Print "Example"
If we know that our data is a string then we can use the GetStringAsync
extension method.
var stringValue = await cache.GetStringAsync("string", token);
Console.WriteLine(stringValue); // Print "Example"
From the code, we can see that all GetStringAsync
does is calling GetAsync
, decoding the response using UTF-8, and returning the value.
byte[] data = await cache.GetAsync(key, token).ConfigureAwait(false);
if (data == null)
{
return null;
}
return Encoding.UTF8.GetString(data, 0, data.Length);
In all cases, if the cache key does not exist then the return value will be null
.
Refresh a cache entry
As mentioned in a previous section, we can set a SlidingExpiration value so that a cache entry will expire if it is not accessed within a sliding window. Sometimes, we want to reset that sliding window without retrieving or updating the value. In that case, we can use the RefreshAsync
method.
var token = <code to create a cancellation token> // can omit this if you don't need to cancel the request
await cache.RefreshAsync("string", token);
Note that if we don’t set the SlidingExpiration value when writing an entry then when we call RefreshAsync
on that entry, the method will do nothing.
Implementing a GetOrSetAsync method
Just like in the first part, we will write a GetOrSetAsync
extension method.
public async Task<byte[]> GetOrSetAsync(
this IDistributedCache cache,
string key,
Func<Task<byte[]>> factory,
DistributedCacheEntryOptions cacheOptions = default(DistributedCacheEntryOptions),
CancellationToken token = default(CancellationToken))
{
// Use key to retrieve the data from cache
var cacheValue = await cache.GetAsync(key, token);
// If the result is not null
if (cacheValue is object)
{
// Then that key already exists, return its value
return cacheValue;
}
// Otherwise, execute the delegate to create a new value
cacheValue = await factory();
if (cacheOptions is null)
{
cacheOptions = new DistributedCacheEntryOptions();
}
// Cache it
await cache.SetAsync(key, cacheValue, cacheOptions, token);
// Then return that new value
return cacheValue;
}
We can use it like this.
var timeStamp = Encoding.UTF8.GetBytes(DateTime.Now.ToString());
await cache.GetOrSetAsync("GetOrSetAsync", () => Task.FromResult(timeStamp));
var rs = await cache.GetAsync("GetOrSetAsync");
Console.WriteLine(Encoding.UTF8.GetString(rs));
No matter how many times we execute the code above, the result won’t change.
<The first time we execute that code>
Conclusion
In this article, we’ve checked out the StackExchangeRedis package and see how it can help us make non-blocking calls to a Redis server. In part three, we will try to exchange data between a Python app and a C# .NET app via Redis.
awesome like other posts
Thank you for your support.