24. Redis for distributed caching
- Redis for distributed caching
About this chapter
Improve API performance using Redis for distributed caching with the cache-aside pattern, managing expiration strategies and cache invalidation.
- When to cache: Identifying cacheable data based on frequency and cost
- Redis setup: Running and connecting to Redis in your infrastructure
- Cache-aside pattern: Check cache, fall back to database, populate cache
- Cache expiration: Absolute expiration, manual invalidation, and staleness
- Key naming conventions: Organizing cache keys for clarity and isolation
- Cache invalidation: Keeping cached data fresh when underlying data changes
Learning outcomes:
- Understand caching benefits and trade-offs
- Set up Redis and create connections in ASP.NET Core
- Implement the cache-aside pattern for read operations
- Choose appropriate cache expiration strategies
- Handle cache misses and database fallback
- Invalidate cache when data changes
24.1 When to Cache
Caching trades memory for speed. Cache data that is:
- Frequently accessed - no point caching data nobody reads
- Expensive to compute - queries, external API calls, calculations
- Acceptable to be stale - exact freshness isn’t critical (e.g., user counts, product details)
Never cache:
- Real-time data (stock prices, live scores)
- User-specific sensitive data
- Small datasets that are fast anyway
- Anything requiring guaranteed accuracy
24.2 Redis Setup
Redis is already in your docker-compose.yaml. It’s a fast in-memory data store used for caching, sessions, and temporary data.
# Start Redis
docker-compose up -d redis
# Verify connection
redis-cli ping
# Output: PONG
24.3 Cache-Aside Pattern (Existing Implementation)
The cache-aside pattern is simple: check cache first, fall back to database if miss, then populate cache.
// Current implementation in PgSqlRegistrationRepository
public class PgSqlRegistrationRepository : IRegistrationRepository
{
private readonly AppDbContext _context;
private readonly IDatabase _redisDb;
public async Task<KeyRegistration?> GetRegistrationByIndex(string keyIndex)
{
// 1. Check cache
var cachedRegistration = await _redisDb.StringGetAsync($"KeyRegistration:{keyIndex}");
if (!cachedRegistration.IsNullOrEmpty)
{
return JsonConvert.DeserializeObject<KeyRegistration>(cachedRegistration!);
}
// 2. Cache miss—fetch from database
var keyRegistration = await _context.keyRegistrations
.FirstOrDefaultAsync(k => k.KeyIndex.ToString() == keyIndex);
// 3. Store in cache if found
if (keyRegistration != null)
{
TimeSpan expiry = TimeSpan.FromHours(1);
await _redisDb.StringSetAsync(
$"KeyRegistration:{keyRegistration.KeyIndex}",
JsonConvert.SerializeObject(keyRegistration),
expiry);
}
return keyRegistration;
}
}
Why this pattern: Simple, works with any database, cache failures are graceful (just slower).
24.4 Cache Expiration
Data in cache expires based on your strategy:
// Absolute expiration: Expires after fixed time
await _redisDb.StringSetAsync(
key: "platform:1",
value: platformJson,
expiry: TimeSpan.FromHours(1)); // Expires in 1 hour regardless
// No expiration: Cache until manually cleared
await _redisDb.StringSetAsync(
key: "config:app",
value: configJson);
// Manual invalidation when data changes
await _redisDb.KeyDeleteAsync("platform:1");
Choose expiration based on data freshness requirements:
- Platforms: 1 hour (not critical if slightly stale)
- API keys: 1 hour (validation is fast anyway)
- Config: No expiration (manually invalidated on changes)
24.5 Applying Cache-Aside to Platforms
The code comment suggests caching platforms. Here’s the implementation:
public class PgSqlCommandRepository : ICommandRepository
{
private readonly AppDbContext _context;
private readonly IDatabase _redisDb;
private const string PlatformCachePrefix = "platform:";
private const int PlatformCacheDurationHours = 1;
public async Task<Platform?> GetPlatformByIdAsync(int id)
{
var cacheKey = $"{PlatformCachePrefix}{id}";
// Try cache first
var cached = await _redisDb.StringGetAsync(cacheKey);
if (!cached.IsNullOrEmpty)
{
return JsonConvert.DeserializeObject<Platform>(cached!);
}
// Cache miss—fetch from database
var platform = await _context.Platforms
.Include(p => p.Commands)
.FirstOrDefaultAsync(p => p.Id == id);
// Store in cache if found
if (platform != null)
{
await _redisDb.StringSetAsync(
cacheKey,
JsonConvert.SerializeObject(platform),
TimeSpan.FromHours(PlatformCacheDurationHours));
}
return platform;
}
// When updating/deleting, invalidate the cache
public async Task UpdatePlatformAsync(Platform platform)
{
await _redisDb.KeyDeleteAsync($"{PlatformCachePrefix}{platform.Id}");
}
public void DeletePlatform(Platform platform)
{
_context.Platforms.Remove(platform);
_redisDb.KeyDelete($"{PlatformCachePrefix}{platform.Id}");
}
}
Key principle: When data changes, invalidate its cache entry so the next request fetches fresh data.
24.6 Cache Invalidation Patterns
Single Item Invalidation (most common):
[HttpPut("{id}")]
public async Task<ActionResult> UpdatePlatform(int id, PlatformMutateDto dto)
{
var platform = await _repository.GetPlatformByIdAsync(id);
_mapper.Map(dto, platform);
await _repository.UpdatePlatformAsync(platform);
await _repository.SaveChangesAsync();
// Clear cache for this platform
await _redisDb.KeyDeleteAsync($"platform:{id}");
return NoContent();
}
Related Items Invalidation (when deleting a parent affects children):
[HttpDelete("{id}")]
public async Task<ActionResult> DeletePlatform(int id)
{
var platform = await _repository.GetPlatformByIdAsync(id);
_repository.DeletePlatform(platform);
await _repository.SaveChangesAsync();
// Invalidate platform and all its commands
await _redisDb.KeyDeleteAsync($"platform:{id}");
foreach (var cmd in platform.Commands)
{
await _redisDb.KeyDeleteAsync($"command:{cmd.Id}");
}
return NoContent();
}
Pattern Invalidation (clear all matching keys):
public async Task InvalidateAllPlatformCache()
{
var server = _redis.GetServer(_redis.GetEndPoints().First());
var keys = server.Keys(pattern: "platform:*"); // All platform cache entries
foreach (var key in keys)
{
await _redisDb.KeyDeleteAsync(key);
}
}