Note: phiên bản Tiếng Việt của bài này ở link dưới.
https://duongnt.com/sliding-expiration-stackexchangeredis-vie
This is a bonus article for the series about Redis-based distributed caching. We will look at how sliding expiration is implemented in StackExchangeRedis. You can find the other parts from here.
Overview of cache expiration in Redis
As a reminder, in Redis we use the EXPIRE command to set a cache entry to expire.
EXPIRE <cache key> <seconds>
This command will remove a cache entry after exactly seconds
has passed.
Alternatively, we can use the EXPIREAT to set our entry to expire at a given time in the future.
EXPIREAT <cache key> <expire time>
Here, expire time
is given as an absolute Unix timestamp, which is the number of seconds since 1970/01/01 00:00:00
.
When an entry is added/updated
In part two, we have looked at the script that StackExchangeRedis calls whenever it sets a cache value.
@"
redis.call('HSET', 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 see that along with the cache value, the absolute expiration time is written into the absexp
field of the hash. And sliding expiration is written into the sldexp
field. But to find out how these values are used, we need to see how StackExchangeRedis calls that script.
await _cache.ScriptEvaluateAsync(SetScript, new RedisKey[] { _instance + key },
new RedisValue[]
{
absoluteExpiration?.Ticks ?? NotPresent,
options.SlidingExpiration?.Ticks ?? NotPresent,
GetExpirationInSeconds(creationTime, absoluteExpiration, options) ?? NotPresent,
value
}).ConfigureAwait(false);
When ARGV[3] != -1
, the EXPIRE
command is called with ARGV[3]
as the number of seconds this entry can live. And ARGV[3]
in this case is the output of the GetExpirationInSeconds method.
return (long)Math.Min(
(absoluteExpiration.Value - creationTime).TotalSeconds,
options.SlidingExpiration.Value.TotalSeconds);
StackExchangeRedis calculates the difference between current time and the absolute expiration time, then compares it with the sliding expiration value. The smaller value will be used to set expiration for our entry. This also explains why sliding expiration cannot keep an entry alive past the absolute expiration time.
What about sliding expiration
From the way expiration of a Redis cache entry works, it’s clear that StackExchangeRedis has to perform additional work everytime we access an entry and update its expiration time. There are two ways to access an entry, either by reading its value or by explicitly refreshing it without reading anything. In the first case we use the GetAsync method, and in the second case we use RefreshAsync.
We can see that in both case, we end up calling a GetAndRefreshAsync method. The only difference is whether we set the getData
flag to true
or false
.
In both cases, if the cache entry exists, we retrieve the absolute expiration time and sliding expiration time from Redis. Then we use them to call a private RefreshAsync
method.
if (results.Length >= 2)
{
MapMetadata(results, out DateTimeOffset? absExpr, out TimeSpan? sldExpr);
await RefreshAsync(key, absExpr, sldExpr, token).ConfigureAwait(false);
}
Inside the private RefreshAsync method, we call the EXPIRE
command to update the expiration time on Redis (if needed). Notice the check to make sure that sliding expiration won’t keep an entry alive past absolute expiration time.
if (sldExpr.HasValue)
{
if (absExpr.HasValue)
{
var relExpr = absExpr.Value - DateTimeOffset.Now;
expr = relExpr <= sldExpr.Value ? relExpr : sldExpr;
}
else
{
expr = sldExpr;
}
await _cache.KeyExpireAsync(_instance + key, expr).ConfigureAwait(false);
// TODO: Error handling
}
And this is how sliding expiration is implemented inside StackExchangeRedis.
Conclusion
I found reading about the inner working of StackExchangeRedis to be quite amusing. And it helps me gain some valuable insight about how Redis works in general. As a side note, I sure hope they can work on the TODO: Error handling
soon :).
One Thought on “Sliding expiration in StackExchangeRedis”