EngineeringSolution ArchitectData and Decisions
Session 02 - Redis Caching Patterns
3 min read8 lessons in this module
Azure Cache for Redis Tiers
| Tier | vCPU | Memory | SLA | Use Case |
|---|
| Basic | Shared | 250MB - 53GB | No SLA | Dev/test |
| Standard | Dedicated | 250MB - 53GB | 99.9% | Production cơ bản |
| Premium | Dedicated | 6GB - 120GB | 99.9% | High throughput, persistence, VNet |
| Enterprise | Dedicated | 12GB - 2TB | 99.99% | Redis modules, active geo-replication |
| Enterprise Flash | Dedicated | 384GB - 1.5TB | 99.99% | Large datasets, cost-effective |
Recommendation: Standard cho hầu hết workloads. Premium khi cần VNet integration hoặc data persistence. Enterprise cho mission-critical.
Cache-Aside Pattern với IDistributedCache
Pattern Overview
Implementation
csharp
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
options.InstanceName = "OrderService:";
});
public class CacheAsideService
{
private readonly IDistributedCache _cache;
private readonly ILogger<CacheAsideService> _logger;
public CacheAsideService(IDistributedCache cache, ILogger<CacheAsideService> logger)
{
_cache = cache;
_logger = logger;
}
public async Task<T?> GetOrSetAsync<T>(
string key,
Func<Task<T?>> factory,
TimeSpan? absoluteExpiration = null,
TimeSpan? slidingExpiration = null,
CancellationToken ct = default) where T : class
{
var cached = await _cache.GetStringAsync(key, ct);
if (cached is not null)
{
_logger.LogDebug("Cache HIT: {Key}", key);
return JsonSerializer.Deserialize<T>(cached);
}
_logger.LogDebug("Cache MISS: {Key}", key);
var value = await factory();
if (value is null) return null;
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = absoluteExpiration ?? TimeSpan.FromMinutes(10),
SlidingExpiration = slidingExpiration ?? TimeSpan.FromMinutes(2)
};
var serialized = JsonSerializer.Serialize(value);
await _cache.SetStringAsync(key, serialized, options, ct);
return value;
}
public async Task InvalidateAsync(string key, CancellationToken ct = default)
{
await _cache.RemoveAsync(key, ct);
_logger.LogDebug("Cache INVALIDATED: {Key}", key);
}
public async Task InvalidateByPrefixAsync(string prefix)
{
_logger.LogDebug("Cache INVALIDATED prefix: {Prefix}*", prefix);
}
}
[HttpGet("orders/{id}")]
public async Task<IActionResult> GetOrder(int id)
{
var order = await _cacheService.GetOrSetAsync(
$"order:{id}",
() => _orderRepository.GetByIdAsync(id),
absoluteExpiration: TimeSpan.FromMinutes(15));
return order is null ? NotFound() : Ok(order);
}
Session Store
csharp
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30);
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.IsEssential = true;
});
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
options.InstanceName = "Session:";
});
Pub/Sub for Real-time
csharp
using StackExchange.Redis;
public class RedisPubSubService
{
private readonly IConnectionMultiplexer _redis;
public RedisPubSubService(IConnectionMultiplexer redis) => _redis = redis;
public async Task PublishOrderUpdateAsync(string orderId, string status)
{
var subscriber = _redis.GetSubscriber();
var message = JsonSerializer.Serialize(new { orderId, status, timestamp = DateTime.UtcNow });
await subscriber.PublishAsync(RedisChannel.Literal("order-updates"), message);
}
public async Task SubscribeToOrderUpdatesAsync(Action<string> handler)
{
var subscriber = _redis.GetSubscriber();
await subscriber.SubscribeAsync(
RedisChannel.Literal("order-updates"),
(channel, message) => handler(message!));
}
}
Redis Data Structures - Use Cases
| Structure | Command | Use Case | Ví dụ |
|---|
| String | GET/SET | Cache objects, counters | Session data, API response cache |
| Hash | HGET/HSET | Object với multiple fields | User profile (name, email, role) |
| List | LPUSH/RPOP | Queue, recent items | Recent activity feed |
| Set | SADD/SMEMBERS | Unique collections | Online users, tags |
| Sorted Set | ZADD/ZRANGE | Ranked data | Leaderboard, scheduled tasks |
| Stream | XADD/XREAD | Event streaming | Event log, message queue |
Ví dụ: Leaderboard với Sorted Set
csharp
public class LeaderboardService
{
private readonly IDatabase _db;
private const string Key = "leaderboard:weekly";
public LeaderboardService(IConnectionMultiplexer redis)
=> _db = redis.GetDatabase();
public async Task AddScoreAsync(string userId, double score)
{
await _db.SortedSetAddAsync(Key, userId, score);
}
public async Task<IEnumerable<LeaderboardEntry>> GetTopAsync(int count = 10)
{
var entries = await _db.SortedSetRangeByRankWithScoresAsync(
Key, 0, count - 1, Order.Descending);
return entries.Select((e, i) => new LeaderboardEntry
{
Rank = i + 1,
UserId = e.Element.ToString(),
Score = e.Score
});
}
public async Task<long?> GetRankAsync(string userId)
{
var rank = await _db.SortedSetRankAsync(Key, userId, Order.Descending);
return rank.HasValue ? rank.Value + 1 : null;
}
}
Connection Pooling - StackExchange.Redis
csharp
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;
config.AsyncTimeout = 5000;
config.ReconnectRetryPolicy = new ExponentialRetry(5000);
config.KeepAlive = 60;
config.Ssl = true;
config.SslProtocols = System.Security.Authentication.SslProtocols.Tls12;
return ConnectionMultiplexer.Connect(config);
});
builder.Services.AddScoped<IDatabase>(sp =>
sp.GetRequiredService<IConnectionMultiplexer>().GetDatabase());
Connection Best Practices
| Practice | Mô tả |
|---|
| Singleton multiplexer | NEVER tạo nhiều instances - memory leak |
| AbortOnConnectFail = false | Cho phép retry thay vì crash |
| Handle RedisTimeoutException | Implement retry với exponential backoff |
| Monitor connections | Track ConnectionMultiplexer.GetCounters() |
| Use firewall rules | Restrict access tới known IPs |
TTL Strategies
| Strategy | Pattern | Ví dụ |
|---|
| Fixed TTL | Mọi key cùng TTL | Product catalog: 1 giờ |
| Sliding TTL | Reset TTL mỗi lần access | Session data: 30 phút sliding |
| Write-through | Update cache khi write DB | Order status: immediate consistency |
| TTL with jitter | TTL ± random offset | Tránh thundering herd khi nhiều keys expire cùng lúc |
| No TTL + explicit invalidation | Invalidate khi data thay đổi | User settings: cache forever, invalidate on update |
Thundering Herd Prevention
csharp
private static TimeSpan AddJitter(TimeSpan baseTtl)
{
var jitter = TimeSpan.FromSeconds(Random.Shared.Next(0, 60));
return baseTtl + jitter;
}
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = AddJitter(TimeSpan.FromMinutes(10))
};
Trade-offs
| Quyết định | Pro | Con |
|---|
| Redis vs in-memory cache | Shared across instances, persistent | Network latency, extra infra |
| Short TTL | Data luôn fresh | Cache hit rate thấp, more DB load |
| Long TTL | High cache hit rate | Stale data risk |
| Cache-aside | Simple, app controls | Cache miss penalty, consistency gaps |
| Write-through | Strong consistency | Write latency tăng |
| Pub/Sub vs Service Bus | Ultra-low latency | No persistence, at-most-once delivery |
| Premium tier | Persistence, VNet | Cost gấp 4-5x Standard |