Redis Caching Advanced
Master advanced Redis caching patterns and strategies.
By EMEPublished: February 20, 2025
rediscachingperformancedata structures
A Simple Analogy
Redis is like a super-fast notepad for your application. Instead of searching a library (database), you grab a sticky note with the answer.
Why Redis?
- Speed: In-memory, microsecond latency
- Patterns: Hash, set, list, sorted set, streams
- Expiration: Auto-delete old data
- Pub/Sub: Real-time messaging
- Transactions: Atomic operations
Basic Operations
var redis = ConnectionMultiplexer.Connect("localhost:6379");
var db = redis.GetDatabase();
// String
db.StringSet("user:1:name", "Alice");
var name = db.StringGet("user:1:name");
// Hash (related fields)
db.HashSet("user:1", new HashEntry[]
{
new("name", "Alice"),
new("email", "alice@example.com"),
new("age", "30")
});
var all = db.HashGetAll("user:1");
var email = db.HashGet("user:1", "email");
// List
db.ListPush("notifications:user:1", "New message");
db.ListPush("notifications:user:1", "Order shipped");
var latest = db.ListRange("notifications:user:1", 0, 9);
// Set (unique items, fast membership test)
db.SetAdd("followers:alice", "bob");
db.SetAdd("followers:alice", "charlie");
var hasFollower = db.SetContains("followers:alice", "bob");
var allFollowers = db.SetMembers("followers:alice");
// Sorted Set (ordered by score)
db.SortedSetAdd("leaderboard", new SortedSetEntry[]
{
new("alice", 1000),
new("bob", 950),
new("charlie", 890)
});
var top3 = db.SortedSetRangeByRankWithScores("leaderboard", 0, 2, Order.Descending);
Cache-Aside Pattern
public class UserRepository
{
private readonly IDistributedCache _cache;
private readonly IDatabase _db;
public async Task<User> GetUserAsync(int id)
{
var cacheKey = $"user:{id}";
// Try cache first
var cached = await _cache.GetStringAsync(cacheKey);
if (!string.IsNullOrEmpty(cached))
return JsonSerializer.Deserialize<User>(cached);
// Not in cache, get from database
var user = await _db.Users.FindAsync(id);
if (user != null)
{
// Store in cache for 1 hour
await _cache.SetStringAsync(
cacheKey,
JsonSerializer.Serialize(user),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
});
}
return user;
}
}
Cache Invalidation
public class UserService
{
private readonly IDistributedCache _cache;
public async Task UpdateUserAsync(User user)
{
// Update database
await _db.SaveAsync(user);
// Invalidate cache
await _cache.RemoveAsync($"user:{user.Id}");
await _cache.RemoveAsync($"user-email:{user.Email}");
}
// Use tags for related cache entries
public async Task InvalidateUserRelatedAsync(int userId)
{
var pattern = $"user:{userId}:*";
// Remove all related keys
var server = _redis.GetServer(_redis.GetEndPoints().First());
var keys = server.Keys(pattern: pattern);
foreach (var key in keys)
{
await _cache.RemoveAsync(key.ToString());
}
}
}
Pub/Sub Messaging
// Subscriber
var subscriber = redis.GetSubscriber();
await subscriber.SubscribeAsync("orders", (channel, message) =>
{
var order = JsonSerializer.Deserialize<Order>(message.ToString());
ProcessOrder(order);
});
// Publisher
var orders = redis.GetSubscriber();
var newOrder = new Order { Id = 123, Total = 99.99 };
await orders.PublishAsync("orders", JsonSerializer.Serialize(newOrder));
// Multiple channels
await subscriber.SubscribeAsync(new[]
{
new ChannelMessageQueue("orders"),
new ChannelMessageQueue("notifications"),
new ChannelMessageQueue("events")
}, (channel, message) =>
{
HandleMessage(channel, message);
});
Streams (Event Log)
var db = redis.GetDatabase();
// Add to stream
var eventId = await db.StreamAddAsync("events", new NameValueEntry[]
{
new("action", "order-created"),
new("order-id", "123"),
new("timestamp", DateTime.UtcNow.ToString("O"))
});
// Read stream
var entries = await db.StreamRangeAsync("events", "-", "+"); // All entries
var lastN = await db.StreamRangeAsync("events", "-", "+", take: 10); // Last 10
// Consumer groups
await db.StreamCreateConsumerGroupAsync("events", "order-service", StreamPosition.NewMessages);
var message = await db.StreamReadGroupAsync(
"events",
"order-service",
"consumer-1",
count: 1);
Distributed Lock
public class RedisLock : IDisposable
{
private readonly IDatabase _db;
private readonly string _lockKey;
private readonly string _lockValue;
public RedisLock(IDatabase db, string key, TimeSpan expiration)
{
_db = db;
_lockKey = key;
_lockValue = Guid.NewGuid().ToString();
var acquired = _db.StringSet(_lockKey, _lockValue, expiration, When.NotExists);
if (!acquired)
throw new InvalidOperationException("Could not acquire lock");
}
public void Dispose()
{
var tx = _db.CreateTransaction();
tx.AddCondition(Condition.StringEqual(_lockKey, _lockValue));
tx.StringDeleteAsync(_lockKey);
tx.Execute();
}
}
// Usage
using (new RedisLock(db, "user:1:update", TimeSpan.FromSeconds(5)))
{
// Only one process can enter here at a time
var user = await GetUserAsync(1);
user.Balance -= 100;
await SaveUserAsync(user);
}
Best Practices
- Use appropriate data structures: Hash for objects, Set for tags
- Set expiration: Prevent unbounded memory
- Monitor memory: Redis stores everything in RAM
- Use pipelining: Batch commands for efficiency
- Handle connection failures: Graceful degradation
Related Concepts
- Memcached (simpler alternative)
- Cache warming strategies
- Cache stampede prevention
- Message queues in Redis
Summary
Redis provides blazing-fast in-memory caching with advanced data structures. Master cache-aside patterns, pub/sub messaging, and streams to build responsive, scalable systems.