DevOps & Cloud
Redis Cache Management: .NET Core & Performance Optimization
Redis is a high-performance in-memory data structure store that is indispensable for distributed cache, session management, message queues, and real-time applications. In the .NET ecosystem, it integrates seamlessly through the IDistributedCache interface and serves as a SignalR backplane to enable real-time communication across multiple server instances. In this article, I will examine Redis cache strategies, .NET integration, and production best practices in detail.
In my projects, I have experienced up to 80% reduction in API response times and significant database load decrease by using Redis. The right cache strategy can dramatically improve your application's performance.
Redis .NET Integration
Basic Usage with IDistributedCache
.NET's IDistributedCache interface provides a provider-agnostic abstraction. Switching to Redis requires only a registration change.
// Program.cs - Redis cache registration
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
options.InstanceName = "MyApp:";
});
// Cache usage in the service layer
public class ProductService
{
private readonly IDistributedCache _cache;
private readonly ApplicationDbContext _context;
private static readonly DistributedCacheEntryOptions CacheOptions = new()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
SlidingExpiration = TimeSpan.FromMinutes(10)
};
public ProductService(IDistributedCache cache, ApplicationDbContext context)
{
_cache = cache;
_context = context;
}
public async Task<ProductDto?> GetProductAsync(int id, CancellationToken ct)
{
var cacheKey = $"product:{id}";
// Read from cache
var cached = await _cache.GetStringAsync(cacheKey, ct);
if (cached is not null)
return JsonSerializer.Deserialize<ProductDto>(cached);
// Fetch from database and write to cache
var product = await _context.Products
.Where(p => p.Id == id)
.Select(p => new ProductDto(p.Id, p.Name, p.Price))
.FirstOrDefaultAsync(ct);
if (product is not null)
{
var json = JsonSerializer.Serialize(product);
await _cache.SetStringAsync(cacheKey, json, CacheOptions, ct);
}
return product;
}
public async Task InvalidateProductCacheAsync(int id, CancellationToken ct)
{
await _cache.RemoveAsync($"product:{id}", ct);
}
}Advanced Operations with StackExchange.Redis
While IDistributedCache is sufficient for simple scenarios, you can use the StackExchange.Redis library directly to leverage the full power of Redis:
// Redis connection multiplexer registration
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
var config = ConfigurationOptions.Parse(
builder.Configuration.GetConnectionString("Redis")!);
config.AbortOnConnectFail = false;
config.ConnectRetry = 3;
config.ConnectTimeout = 5000;
config.SyncTimeout = 3000;
return ConnectionMultiplexer.Connect(config);
});
// Advanced Redis operations
public class RedisCacheService
{
private readonly IDatabase _db;
public RedisCacheService(IConnectionMultiplexer redis)
{
_db = redis.GetDatabase();
}
// Sorted Set for leaderboard
public async Task UpdateLeaderboardAsync(string userId, double score)
{
await _db.SortedSetAddAsync("leaderboard", userId, score);
}
public async Task<List<LeaderboardEntry>> GetTopPlayersAsync(int count)
{
var entries = await _db.SortedSetRangeByRankWithScoresAsync(
"leaderboard", 0, count - 1, Order.Descending);
return entries.Select(e => new LeaderboardEntry(
e.Element.ToString(), e.Score)).ToList();
}
// Hash for user sessions
public async Task SetUserSessionAsync(string sessionId, UserSession session)
{
var hashEntries = new HashEntry[]
{
new("userId", session.UserId),
new("email", session.Email),
new("role", session.Role),
new("loginAt", session.LoginAt.ToString("O"))
};
await _db.HashSetAsync($"session:{sessionId}", hashEntries);
await _db.KeyExpireAsync($"session:{sessionId}", TimeSpan.FromHours(24));
}
// Distributed lock
public async Task<bool> AcquireLockAsync(string resource, TimeSpan expiry)
{
return await _db.StringSetAsync(
$"lock:{resource}", Environment.MachineName, expiry, When.NotExists);
}
}SignalR Redis Backplane
Real-Time Communication in Multi-Server Environments
When your application runs across multiple servers, SignalR messages need to be delivered to all servers. The Redis backplane solves this problem.
// Program.cs - SignalR Redis backplane configuration
builder.Services.AddSignalR()
.AddStackExchangeRedis(builder.Configuration.GetConnectionString("Redis")!, options =>
{
options.Configuration.ChannelPrefix = RedisChannel.Literal("MyApp");
});
// SignalR Hub
public class NotificationHub : Hub
{
public async Task SendToGroup(string groupName, string message)
{
// This message is relayed to all servers via Redis
await Clients.Group(groupName).SendAsync("ReceiveNotification", message);
}
public async Task JoinGroup(string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
}
}Cache Strategies
Cache-Aside (Lazy Loading)
The most common strategy. Data is first read from cache; if not found, it is fetched from the source and written to cache. The ProductService example above uses this pattern.
Write-Through
Both the database and cache are updated simultaneously during write operations. Preferred in scenarios where data consistency is critical.
Cache Invalidation Strategies
Cache invalidation is one of the hardest problems in distributed systems. Incorrect invalidation leads to stale data or unnecessary cache misses:
- TTL (Time to Live): Simple invalidation through automatic expiration
- Event-based: Actively clearing cache when data changes
- Tag-based: Grouping related cache entries for bulk invalidation
- Pattern-based: Bulk deletion based on key patterns
// Pattern-based cache invalidation example
public class CacheInvalidationService
{
private readonly IConnectionMultiplexer _redis;
public CacheInvalidationService(IConnectionMultiplexer redis)
{
_redis = redis;
}
// Invalidate cache for all products in a specific category
public async Task InvalidateCategoryProductsAsync(int categoryId)
{
var server = _redis.GetServer(_redis.GetEndPoints().First());
var db = _redis.GetDatabase();
var keys = server.Keys(pattern: $"MyApp:product:category:{categoryId}:*");
foreach (var key in keys)
{
await db.KeyDeleteAsync(key);
}
}
// Write-through pattern: update cache and DB simultaneously
public async Task UpdateProductWithCacheAsync(
int id, ProductUpdateDto dto, ApplicationDbContext context)
{
var db = _redis.GetDatabase();
// Update database
var product = await context.Products.FindAsync(id);
if (product is null) return;
product.Name = dto.Name;
product.Price = dto.Price;
await context.SaveChangesAsync();
// Update cache (write-through)
var cacheKey = $"MyApp:product:{id}";
var json = JsonSerializer.Serialize(new ProductDto(id, dto.Name, dto.Price));
await db.StringSetAsync(cacheKey, json, TimeSpan.FromMinutes(30));
}
}Redis Streams for Event-Driven Architecture
Redis Streams allows you to build a Kafka-like messaging system on top of Redis. With consumer groups, messages can be distributed among multiple consumers:
// Publishing messages with Redis Streams
public async Task PublishOrderEventAsync(OrderEvent orderEvent)
{
var db = _redis.GetDatabase();
var entries = new NameValueEntry[]
{
new("type", orderEvent.Type),
new("orderId", orderEvent.OrderId.ToString()),
new("data", JsonSerializer.Serialize(orderEvent)),
new("timestamp", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString())
};
await db.StreamAddAsync("order-events", entries, maxLength: 10000);
}
// Reading messages with consumer groups
public async Task ConsumeOrderEventsAsync(CancellationToken ct)
{
var db = _redis.GetDatabase();
var groupName = "order-processor";
var consumerName = Environment.MachineName;
// Create consumer group (if not exists)
try { await db.StreamCreateConsumerGroupAsync("order-events", groupName, "0"); }
catch (RedisServerException) { /* Group already exists */ }
while (!ct.IsCancellationRequested)
{
var entries = await db.StreamReadGroupAsync(
"order-events", groupName, consumerName, ">", count: 10);
foreach (var entry in entries)
{
// Process the message
var data = entry.Values.First(v => v.Name == "data").Value;
await ProcessOrderEventAsync(data.ToString());
// Acknowledge the message
await db.StreamAcknowledgeAsync("order-events", groupName, entry.Id);
}
await Task.Delay(100, ct);
}
}Redis Monitoring and Performance Analysis
Monitoring with redis-cli
In production environments, redis-cli is a powerful tool for monitoring Redis performance:
# Live command monitoring (caution: can impact performance in production)
redis-cli MONITOR
# Redis server statistics
redis-cli INFO stats
# Memory usage details
redis-cli INFO memory
# List connected clients
redis-cli CLIENT LIST
# Log slow queries (slower than 10ms)
redis-cli CONFIG SET slowlog-log-slower-than 10000
redis-cli SLOWLOG GET 10
# Key count and size analysis
redis-cli DBSIZE
redis-cli INFO keyspace
# Analyze memory usage for key patterns
redis-cli --bigkeysMemory Optimization
Apply these strategies to optimize Redis memory usage:
- maxmemory-policy:
allkeys-lruis the most common choice, deleting the least recently accessed keys when memory is full - Key expiration: Set appropriate TTL for each key; avoid keys without expiration
- Data compression: Compress large JSON values with LZ4 or Gzip to reduce storage space
- Hash vs String: For small objects with few fields, the hash data structure is more memory-efficient
- Key naming: Use short but meaningful key names to prevent memory waste
Redis Configuration Recommendations
Important settings for production environments:
- maxmemory-policy: Use
allkeys-lruorvolatile-lrufor memory management - appendonly: Enable AOF (Append Only File) for data persistence
- Sentinel/Cluster: Use Redis Sentinel or Cluster mode for high availability
- Connection pooling: Use ConnectionMultiplexer as a singleton; never open a new connection per request
Practical Tips and Summary
Keep these points in mind for Redis cache management:
- Cache key design: Use consistent and meaningful key naming (e.g.,
entity:id:field) - TTL strategy: Define appropriate expiration times for each data type
- Monitoring: Monitor performance with the Redis INFO command and RedisInsight
- Memory management: Do not set maxmemory to more than 70% of server RAM
- Serialization: Use System.Text.Json for fast and efficient serialization
When used correctly, Redis can multiply your application's performance. The key is choosing the right data structure and cache strategy for each scenario.
Related Articles
Caching Strategies in .NET: In-Memory, Distributed, and Redis
Implement effective caching strategies in .NET. In-memory cache, distributed cache, and Redis integration.
.NET Performance Optimization: Profiling and Best Practices
Optimize .NET application performance. Profiling tools, memory management, and async patterns.
Real-time Apps with SignalR in .NET
Build real-time web applications with SignalR. Hub architecture, group management, Redis backplane, and scaling strategies.
Have a Flutter Project?
I build high-performance Flutter applications for iOS, Android, and web.
Get in Touch