perf(cache): use ValueTask on hot sync paths

This commit is contained in:
2026-04-05 13:40:19 -04:00
parent 7beac7484d
commit af54a3eec1
+39 -14
View File
@@ -170,17 +170,22 @@ public class RedisCacheService
/// Gets a cached value as a string.
/// Checks L1 memory cache first, falls back to L2 Redis.
/// </summary>
public async Task<string?> GetStringAsync(string key)
public ValueTask<string?> GetStringAsync(string key)
{
// L1: Try in-memory cache first (sub-microsecond)
if (TryGetMemoryValue(key, out var memoryValue))
{
_logger.LogDebug("L1 memory cache HIT: {Key}", key);
return memoryValue;
return new ValueTask<string?>(memoryValue);
}
if (!IsEnabled) return null;
if (!IsEnabled) return new ValueTask<string?>((string?)null);
return new ValueTask<string?>(GetStringFromRedisAsync(key));
}
private async Task<string?> GetStringFromRedisAsync(string key)
{
try
{
// L2: Fall back to Redis
@@ -219,7 +224,7 @@ public class RedisCacheService
/// Uses source-generated serializer for registered types (3-8x faster),
/// with automatic fallback to reflection-based serialization.
/// </summary>
public async Task<T?> GetAsync<T>(string key) where T : class
public async ValueTask<T?> GetAsync<T>(string key) where T : class
{
var json = await GetStringAsync(key);
if (string.IsNullOrEmpty(json)) return null;
@@ -243,13 +248,18 @@ public class RedisCacheService
/// Sets a cached value with TTL.
/// Writes to both L1 memory cache and L2 Redis.
/// </summary>
public async Task<bool> SetStringAsync(string key, string value, TimeSpan? expiry = null)
public ValueTask<bool> SetStringAsync(string key, string value, TimeSpan? expiry = null)
{
// Always update L1 (even if Redis is down — provides degraded caching)
SetMemoryValue(key, value, expiry);
if (!IsEnabled) return false;
if (!IsEnabled) return new ValueTask<bool>(false);
return new ValueTask<bool>(SetStringWithRedisAsync(key, value, expiry));
}
private async Task<bool> SetStringWithRedisAsync(string key, string value, TimeSpan? expiry)
{
try
{
return await SetStringInternalAsync(key, value, expiry);
@@ -349,7 +359,7 @@ public class RedisCacheService
/// Uses source-generated serializer for registered types (3-8x faster),
/// with automatic fallback to reflection-based serialization.
/// </summary>
public async Task<bool> SetAsync<T>(string key, T value, TimeSpan? expiry = null) where T : class
public async ValueTask<bool> SetAsync<T>(string key, T value, TimeSpan? expiry = null) where T : class
{
try
{
@@ -386,14 +396,19 @@ public class RedisCacheService
/// <summary>
/// Deletes a cached value from both L1 memory and L2 Redis.
/// </summary>
public async Task<bool> DeleteAsync(string key)
public ValueTask<bool> DeleteAsync(string key)
{
// Always evict from L1
_memoryCache.Remove(key);
_memoryKeys.TryRemove(key, out _);
if (!IsEnabled) return false;
if (!IsEnabled) return new ValueTask<bool>(false);
return new ValueTask<bool>(DeleteFromRedisAsync(key));
}
private async Task<bool> DeleteFromRedisAsync(string key)
{
try
{
return await _db!.KeyDeleteAsync(key);
@@ -408,15 +423,20 @@ public class RedisCacheService
/// <summary>
/// Checks if a key exists.
/// </summary>
public async Task<bool> ExistsAsync(string key)
public ValueTask<bool> ExistsAsync(string key)
{
if (ShouldUseMemoryCache(key) && _memoryCache.TryGetValue(key, out _))
{
return true;
return new ValueTask<bool>(true);
}
if (!IsEnabled) return false;
if (!IsEnabled) return new ValueTask<bool>(false);
return new ValueTask<bool>(ExistsInRedisAsync(key));
}
private async Task<bool> ExistsInRedisAsync(string key)
{
try
{
return await _db!.KeyExistsAsync(key);
@@ -451,11 +471,16 @@ public class RedisCacheService
/// Deletes all keys matching a pattern (e.g., "search:*").
/// WARNING: Use with caution as this scans all keys.
/// </summary>
public async Task<int> DeleteByPatternAsync(string pattern)
public ValueTask<int> DeleteByPatternAsync(string pattern)
{
var memoryDeleted = RemoveMemoryKeysByPattern(pattern);
if (!IsEnabled) return memoryDeleted;
if (!IsEnabled) return new ValueTask<int>(memoryDeleted);
return new ValueTask<int>(DeleteByPatternFromRedisAsync(pattern, memoryDeleted));
}
private async Task<int> DeleteByPatternFromRedisAsync(string pattern, int memoryDeleted)
{
try
{
var server = _redis!.GetServer(_redis.GetEndPoints().First());