Redis Caching Advanced
Master advanced Redis caching patterns and strategies.
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.
Related Articles
ASP.NET Core Caching Strategies
Learn various caching strategies for ASP.NET Core applications.
Read More performanceCaching Strategies and Redis
Implement caching with Redis to improve application performance.
Read More webHTTP Caching Strategies
Leverage HTTP caching to improve performance and reduce bandwidth.
Read More