From 61bc4255ec05f8b4fbd12ea39d3487869f84da80 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Tue, 21 Oct 2025 16:06:18 -0500 Subject: [PATCH 1/3] Improves Redis cache list retrieval performance Optimizes list retrieval by using SortedSetRangeByScoreAsync instead of SortedSetRangeByRankAsync, utilizing timestamp-based scores for efficient expiration management. Handles legacy sets by migrating them to sorted sets when encountering WRONGTYPE exceptions, ensuring compatibility and preventing data loss during the transition. Logs trace information when a sorted set is empty during expiration setting. --- src/Foundatio.Redis/Cache/RedisCacheClient.cs | 78 +++++++++++-------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/src/Foundatio.Redis/Cache/RedisCacheClient.cs b/src/Foundatio.Redis/Cache/RedisCacheClient.cs index 8080d2f..bac34f9 100644 --- a/src/Foundatio.Redis/Cache/RedisCacheClient.cs +++ b/src/Foundatio.Redis/Cache/RedisCacheClient.cs @@ -246,6 +246,7 @@ private CacheValue RedisValueToCacheValue(RedisValue redisValue) } } + public async Task>> GetAllAsync(IEnumerable keys) { if (keys is null) @@ -256,6 +257,7 @@ public async Task>> GetAllAsync(IEnumerable if (redisKeys.Length == 0) return result; + // parallelize? if (_options.ConnectionMultiplexer.IsCluster()) { foreach (var hashSlotGroup in redisKeys.GroupBy(k => _options.ConnectionMultiplexer.HashSlot(k))) @@ -284,19 +286,25 @@ public async Task>> GetListAsync(string key, int? p if (page is < 1) throw new ArgumentOutOfRangeException(nameof(page), "Page cannot be less than 1"); - await RemoveExpiredListValuesAsync(key, typeof(T) == typeof(string)).AnyContext(); - - if (!page.HasValue) + try { - var set = await Database.SortedSetRangeByScoreAsync(key, flags: _options.ReadMode).AnyContext(); - return RedisValuesToCacheValue(set); + if (!page.HasValue) + { + var set = await Database.SortedSetRangeByScoreAsync(key, _timeProvider.GetUtcNow().ToUnixTimeMilliseconds() + 1, flags: _options.ReadMode).AnyContext(); + return RedisValuesToCacheValue(set); + } + else + { + long skip = (page.Value - 1) * pageSize; + var set = await Database.SortedSetRangeByScoreAsync(key, _timeProvider.GetUtcNow().ToUnixTimeMilliseconds() + 1, skip: skip, take: pageSize, flags: _options.ReadMode).AnyContext(); + return RedisValuesToCacheValue(set); + } } - else + catch (RedisServerException ex) when (ex.Message.StartsWith("WRONGTYPE")) { - long start = (page.Value - 1) * pageSize; - long end = start + pageSize - 1; - var set = await Database.SortedSetRangeByRankAsync(key, start, end, flags: _options.ReadMode).AnyContext(); - return RedisValuesToCacheValue(set); + _logger.LogInformation(ex, "Migrating legacy set to sorted set for key: {Key}", key); + await MigrateLegacySetToSortedSetForKeyAsync(key, typeof(T) == typeof(string)).AnyContext(); + return await GetListAsync(key, page, pageSize).AnyContext(); } } @@ -375,7 +383,10 @@ private async Task SetListExpirationAsync(string key) { var items = await Database.SortedSetRangeByRankWithScoresAsync(key, 0, 0, order: Order.Descending).AnyContext(); if (items.Length == 0) + { + _logger.LogTrace("Sorted set is empty for key: {Key}, expiration will not be set", key); return; + } long highestExpirationInMs = (long)items.Single().Score; if (highestExpirationInMs > MaxUnixEpochMilliseconds) @@ -403,32 +414,35 @@ private async Task RemoveExpiredListValuesAsync(string key, bool isStringValu catch (RedisServerException ex) when (ex.Message.StartsWith("WRONGTYPE")) { _logger.LogInformation(ex, "Migrating legacy set to sorted set for key: {Key}", key); + await MigrateLegacySetToSortedSetForKeyAsync(key, isStringValues).AnyContext(); + } + } - // convert legacy set to sorted set - var oldItems = await Database.SetMembersAsync(key).AnyContext(); - await Database.KeyDeleteAsync(key).AnyContext(); - - var currentKeyExpiresIn = await GetExpirationAsync(key).AnyContext(); - if (isStringValues) - { - var oldItemValues = new List(oldItems.Length); - foreach (string oldItem in RedisValuesToCacheValue(oldItems).Value) - oldItemValues.Add(oldItem); - - await ListAddAsync(key, oldItemValues, currentKeyExpiresIn).AnyContext(); - } - else - { - var oldItemValues = new List(oldItems.Length); - foreach (var oldItem in RedisValuesToCacheValue(oldItems).Value) - oldItemValues.Add(oldItem); + private async Task MigrateLegacySetToSortedSetForKeyAsync(string key, bool isStringValues) + { + // convert legacy set to sorted set + var oldItems = await Database.SetMembersAsync(key).AnyContext(); + var currentKeyExpiresIn = await GetExpirationAsync(key).AnyContext(); + await Database.KeyDeleteAsync(key).AnyContext(); + if (isStringValues) + { + var oldItemValues = new List(oldItems.Length); + foreach (string oldItem in RedisValuesToCacheValue(oldItems).Value) + oldItemValues.Add(oldItem); - await ListAddAsync(key, oldItemValues, currentKeyExpiresIn).AnyContext(); - } + await ListAddAsync(key, oldItemValues, currentKeyExpiresIn).AnyContext(); + } + else + { + var oldItemValues = new List(oldItems.Length); + foreach (var oldItem in RedisValuesToCacheValue(oldItems).Value) + oldItemValues.Add(oldItem); - if (currentKeyExpiresIn.HasValue) - await Database.KeyExpireAsync(key, (DateTime?)null).AnyContext(); + await ListAddAsync(key, oldItemValues, currentKeyExpiresIn).AnyContext(); } + + if (currentKeyExpiresIn.HasValue) + await Database.KeyExpireAsync(key, (DateTime?)null).AnyContext(); } public Task SetAsync(string key, T value, TimeSpan? expiresIn = null) From df6857154ecd71c089bb4ce80f6f12e52145bb5f Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Tue, 21 Oct 2025 16:53:41 -0500 Subject: [PATCH 2/3] Improves Redis cache list retrieval performance. Updates the sorted set range query for retrieving cached lists to use Double.PositiveInfinity for the maximum score, ensuring all valid items are returned. This change enhances performance by avoiding unnecessary additions of 1 millisecond to the timestamp, streamlining the retrieval process and improving efficiency. --- src/Foundatio.Redis/Cache/RedisCacheClient.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Foundatio.Redis/Cache/RedisCacheClient.cs b/src/Foundatio.Redis/Cache/RedisCacheClient.cs index bac34f9..dcb64b1 100644 --- a/src/Foundatio.Redis/Cache/RedisCacheClient.cs +++ b/src/Foundatio.Redis/Cache/RedisCacheClient.cs @@ -246,7 +246,6 @@ private CacheValue RedisValueToCacheValue(RedisValue redisValue) } } - public async Task>> GetAllAsync(IEnumerable keys) { if (keys is null) @@ -290,13 +289,13 @@ public async Task>> GetListAsync(string key, int? p { if (!page.HasValue) { - var set = await Database.SortedSetRangeByScoreAsync(key, _timeProvider.GetUtcNow().ToUnixTimeMilliseconds() + 1, flags: _options.ReadMode).AnyContext(); + var set = await Database.SortedSetRangeByScoreAsync(key, _timeProvider.GetUtcNow().ToUnixTimeMilliseconds(), Double.PositiveInfinity, Exclude.Start, flags: _options.ReadMode).AnyContext(); return RedisValuesToCacheValue(set); } else { long skip = (page.Value - 1) * pageSize; - var set = await Database.SortedSetRangeByScoreAsync(key, _timeProvider.GetUtcNow().ToUnixTimeMilliseconds() + 1, skip: skip, take: pageSize, flags: _options.ReadMode).AnyContext(); + var set = await Database.SortedSetRangeByScoreAsync(key, _timeProvider.GetUtcNow().ToUnixTimeMilliseconds(), Double.PositiveInfinity, Exclude.Start, skip: skip, take: pageSize, flags: _options.ReadMode).AnyContext(); return RedisValuesToCacheValue(set); } } @@ -440,9 +439,6 @@ private async Task MigrateLegacySetToSortedSetForKeyAsync(string key, bool is await ListAddAsync(key, oldItemValues, currentKeyExpiresIn).AnyContext(); } - - if (currentKeyExpiresIn.HasValue) - await Database.KeyExpireAsync(key, (DateTime?)null).AnyContext(); } public Task SetAsync(string key, T value, TimeSpan? expiresIn = null) From cb640e034d464aa2eaaf295b1a82f8389e9a55fb Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Tue, 21 Oct 2025 16:55:32 -0500 Subject: [PATCH 3/3] Remove comment --- src/Foundatio.Redis/Cache/RedisCacheClient.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Foundatio.Redis/Cache/RedisCacheClient.cs b/src/Foundatio.Redis/Cache/RedisCacheClient.cs index dcb64b1..4bd075d 100644 --- a/src/Foundatio.Redis/Cache/RedisCacheClient.cs +++ b/src/Foundatio.Redis/Cache/RedisCacheClient.cs @@ -256,7 +256,6 @@ public async Task>> GetAllAsync(IEnumerable if (redisKeys.Length == 0) return result; - // parallelize? if (_options.ConnectionMultiplexer.IsCluster()) { foreach (var hashSlotGroup in redisKeys.GroupBy(k => _options.ConnectionMultiplexer.HashSlot(k)))