ORTA
Caching Patterns
En yaygın pattern. İlk istek DB'den okur ve cache'e yazar. Sonrakiler cache'den döner.
Ne Zaman Redis Cache Kullan / Kullanma
| Kullan | Kullanma | Gerçek Hayat |
|---|---|---|
| Sık okunan, nadir değişen veri (ürün kataloğu, config) | Her request'te değişen veri (anlık stok, canlı teklif) | E-ticaret: Ürün detay sayfası — günde 100K hit, DB'den 50ms, Redis'ten 0.3ms |
| Pahalı hesaplama sonuçları (rapor, aggregation) | Küçük ve hızlı sorgular (indexed PK lookup <2ms) | Fintech: Günlük portföy özeti — hesaplama 3s, cache'ten anında |
| Session, token, rate limit state | ACID garantisi gereken veri (bakiye, sipariş durumu) | SaaS: User permission cache — JWT decode'dan hızlı |
| Multi-instance paylaşımlı state | Tek instance uygulama + in-memory yeterli | Startup: 1 pod'lu API → IMemoryCache yeter, Redis overhead gereksiz |
Anti-pattern: "Her şeyi cache'le" yaklaşımı memory şişirir, invalidation karmaşıklaşır, stale data riski artar. Önce ölç — sadece P95 > 50ms olan ve hit ratio > 80% olacak endpoint'leri cache'le.
Gerçek hayat senaryosu — E-ticaret ürün sayfası: Ürün bilgisi (ad, açıklama, görseller) nadiren değişir → Cache-Aside, TTL 5dk. Stok bilgisi saniyede değişir → cache'leme, event-driven invalidation veya kısa TTL (10s). Fiyat kampanya ile değişir → Write-Through (admin panelden güncelleme anında cache'i de yaz).
Cache-Aside (Lazy Loading)
# Pseudocode:
# 1. GET product:123
# 2. Cache miss → DB'den oku
# 3. SET product:123 "{json}" EX 900
GET product:123
# (nil) → cache miss
SET product:123 '{"id":123,"name":"Laptop","price":15000}' EX 900
GET product:123 # cache hit
public class CachedProductRepository : IProductRepository
{
private readonly IProductRepository _inner;
private readonly IDatabase _redis;
private readonly TimeSpan _ttl = TimeSpan.FromMinutes(15);
public CachedProductRepository(IProductRepository inner, IConnectionMultiplexer mux)
{
_inner = inner;
_redis = mux.GetDatabase();
}
public async Task<Product?> GetByIdAsync(int id, CancellationToken ct = default)
{
var key = $"product:{id}";
// 1. Cache'den oku
var cached = await _redis.StringGetAsync(key);
if (cached.HasValue)
return JsonSerializer.Deserialize<Product>(cached!);
// 2. DB'den oku
var product = await _inner.GetByIdAsync(id, ct);
if (product is null) return null;
// 3. Cache'e yaz
await _redis.StringSetAsync(key,
JsonSerializer.Serialize(product), _ttl);
return product;
}
public async Task UpdateAsync(Product product, CancellationToken ct = default)
{
await _inner.UpdateAsync(product, ct);
// Cache invalidation (write-through alternatifi)
await _redis.KeyDeleteAsync($"product:{product.Id}");
}
}
// DI kayıt (Scrutor decorator)
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.Decorate<IProductRepository, CachedProductRepository>();
Cache Stampede Koruması
Cache expire olduğunda N request aynı anda DB'ye gider. Yukarıdaki diyagramda gösterildiği gibi, lock + double-check pattern'i ile sadece 1 request DB'ye gider, diğerleri cache'ten döner.
# Lock-based pattern:
GET product:123 # nil → cache miss
SET lock:product:123 "1" NX EX 10 # lock al
# DB'den oku, cache'e yaz
DEL lock:product:123 # lock bırak
public async Task<T?> GetWithStampedeProtectionAsync<T>(
string key, Func<Task<T?>> factory, TimeSpan ttl) where T : class
{
// 1. Cache hit?
var cached = await _redis.StringGetAsync(key);
if (cached.HasValue)
return JsonSerializer.Deserialize<T>(cached!);
// 2. Lock al
var lockKey = $"lock:{key}";
var acquired = await _redis.StringSetAsync(
lockKey, "1", TimeSpan.FromSeconds(10), When.NotExists);
if (acquired)
{
try
{
// Double-check (başka thread yazmış olabilir)
cached = await _redis.StringGetAsync(key);
if (cached.HasValue)
return JsonSerializer.Deserialize<T>(cached!);
var value = await factory();
if (value is not null)
await _redis.StringSetAsync(key,
JsonSerializer.Serialize(value), ttl);
return value;
}
finally
{
await _redis.KeyDeleteAsync(lockKey);
}
}
// 3. Lock alınamadı → kısa bekle ve retry
await Task.Delay(50);
cached = await _redis.StringGetAsync(key);
return cached.HasValue ? JsonSerializer.Deserialize<T>(cached!) : null;
}
Cache Invalidation Stratejileri
| Strateji | Ne zaman | Avantaj | Dezavantaj |
|---|---|---|---|
| TTL-based | Read-heavy, stale OK | Basit | Stale window |
| Event-based | Write sonrası DEL | Tutarlı | Karmaşıklık |
| Write-through | Her write'da cache güncelle | Tutarlı | Write yavaşlar |
| Tag-based | Grup invalidation | Esnek | İmplementasyon zor |
HybridCache (.NET 9+)
.NET 9 ile gelen HybridCache API'si: L1 (memory) + L2 (Redis) built-in, stampede protection dahil, GetOrCreateAsync tek satırda.
dotnet add package Microsoft.Extensions.Caching.Hybrid
// Program.cs
builder.Services.AddHybridCache(options =>
{
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(10), // L2 (Redis) TTL
LocalCacheExpiration = TimeSpan.FromMinutes(1) // L1 (memory) TTL
};
options.MaximumPayloadBytes = 1024 * 1024; // 1MB max value
});
// Redis backend (L2)
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
});
public class ProductService
{
private readonly HybridCache _cache;
private readonly AppDbContext _db;
public ProductService(HybridCache cache, AppDbContext db)
{
_cache = cache;
_db = db;
}
// Stampede protection dahil — aynı anda N request gelirse sadece 1 DB call
public async Task<Product?> GetProductAsync(int id, CancellationToken ct = default)
{
return await _cache.GetOrCreateAsync(
$"product:{id}",
async token => await _db.Products.FindAsync(new object[] { id }, token),
new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(15),
LocalCacheExpiration = TimeSpan.FromMinutes(2)
},
cancellationToken: ct);
}
// Invalidation
public async Task InvalidateProductAsync(int id)
{
await _cache.RemoveAsync($"product:{id}");
}
}
| Özellik | IDistributedCache | HybridCache (.NET 9+) |
|---|---|---|
| L1 + L2 | Manuel impl. | Built-in |
| Stampede protection | Yok | Otomatik |
| Serialization | Manuel | Otomatik (System.Text.Json) |
GetOrCreate |
Yok | Tek satır |
.NET 9+ kullanıyorsan
HybridCachetercih et. Manuel L1+L2 yazmaktan daha güvenli ve stampede-proof.
Serialization Stratejisi
Cache'e yazarken serialization formatı throughput'u doğrudan etkiler. Yanlış seçim %40+ latency artışı yapabilir.
| Format | NuGet | Boyut (1KB obj) | Serialize | Deserialize | Human-readable |
|---|---|---|---|---|---|
| System.Text.Json | Built-in | ~1.2KB | ~2.5μs | ~3.0μs | |
| MessagePack | MessagePack | ~0.7KB | ~0.8μs | ~0.9μs | |
| MemoryPack | MemoryPack | ~0.5KB | ~0.3μs | ~0.4μs | |
| protobuf-net | protobuf-net | ~0.6KB | ~1.0μs | ~1.2μs |
// MessagePack ile Redis cache — %60 daha az bandwidth, 3× hızlı serialize
// NuGet: dotnet add package MessagePack
using MessagePack;
[MessagePackObject]
public class Product
{
[Key(0)] public int Id { get; set; }
[Key(1)] public string Name { get; set; } = "";
[Key(2)] public decimal Price { get; set; }
}
public class MessagePackCacheService
{
private readonly IDatabase _redis;
public MessagePackCacheService(IConnectionMultiplexer mux)
=> _redis = mux.GetDatabase();
public async Task SetAsync<T>(string key, T value, TimeSpan ttl)
{
var bytes = MessagePackSerializer.Serialize(value);
await _redis.StringSetAsync(key, bytes, ttl);
}
public async Task<T?> GetAsync<T>(string key)
{
var bytes = await _redis.StringGetAsync(key);
if (!bytes.HasValue) return default;
return MessagePackSerializer.Deserialize<T>(bytes!);
}
}
Ne zaman binary format? >10K ops/s veya value >10KB ise MessagePack/MemoryPack ciddi fark yaratır. <1K ops/s'de System.Text.Json yeterli — debug kolaylığı (redis-cli ile okunabilir value) daha değerli.
Provider Karşılaştırması — Caching Özelinde
| Özellik | AWS ElastiCache | Azure Cache for Redis | Self-Hosted |
|---|---|---|---|
| Max node memory | 635 GB (r7g.16xlarge) | 120 GB (P5) | Donanıma bağlı |
| Cluster mode | Evet (15 shard × 5 replica) | Evet (10 shard) | Evet (sınırsız) |
| Multi-AZ failover | Otomatik (<30s) | Otomatik | Manuel Sentinel/Cluster |
| TLS overhead | ~%5-10 latency | Varsayılan açık, zorunlu | Opsiyonel |
KEYS/FLUSHALL |
Açık (dikkat!) | Açık | Açık |
| Data tiering (SSD) | Enterprise Flash | Redis on Flash (Enterprise) | |
| Backup | Günlük snapshot (S3) | RDB export (her 1-12h) | Manuel RDB/AOF |
maxmemory-policy default |
volatile-lru | volatile-lru | noeviction |
| Connection limit | Node tipine bağlı (65K) | Tier'a bağlı (P5: 40K) | OS limit |
| Monitoring | CloudWatch metrics | Azure Monitor + Redis Insights | Prometheus + Grafana |
Provider-specific pitfall — Azure:
Azure Cache Basic/Standardtier'da cluster yok, tek node. Failover sırasında 15-30s downtime olur. Caching layer'ınız HA gerektiriyorsa minimum Premium tier kullanın.
Provider-specific pitfall — AWS: ElastiCache VPC-only — Lambda'dan erişim için NAT Gateway veya VPC Lambda gerekir. Her bağlantı ENI tüketir.
Gerçek hayat kararı — Startup: Self-hosted Redis ile başla (Docker, 0 maliyet). İlk 10K DAU'ya kadar yeterli. Sonra managed service'e geçmek 1 saat sürer (connection string değişikliği).