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 :).

A software developer from Vietnam and is currently living in Japan.

One Thought on “Sliding expiration in StackExchangeRedis”

Leave a Reply