Isaac.

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

  1. Use appropriate data structures: Hash for objects, Set for tags
  2. Set expiration: Prevent unbounded memory
  3. Monitor memory: Redis stores everything in RAM
  4. Use pipelining: Batch commands for efficiency
  5. 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.