RRedis Handbook

ORTA

Caching Patterns

En yaygın pattern. İlk istek DB'den okur ve cache'e yazar. Sonrakiler cache'den döner.

Kod örneği görünümü Bu sayfadaki eşleşen örnekleri seçilen istemciye göre gösterir.

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)

Application L1 Memory IMemoryCache L2 Redis IDatabase Database HIT (0ms) MISS write-back (TTL) Cache Hit Cache Miss Write-back
# 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ı

❌ Korumasız (Stampede) Redis Cache TTL EXPIRED Req 1 Req 2 Req 3 Req N ... Database OVERLOAD! N × aynı sorgu → DB çökebilir Cold start, TTL expire, popular key → thundering herd ✅ Lock + Double-Check Redis Cache rebuilding... 🔒 LOCK Req 1 Req 2 Req 3 Database 1 query lock winner bekle... bekle... cache yaz cache hit! 1 DB call + (N-1) cache'ten döner SemaphoreSlim (local) veya Redis SETNX (distributed)

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.

HybridCache — GetOrCreateAsync() tek çağrı Application .GetOrCreateAsync() L1 Memory IMemoryCache TTL: 1-5dk (kısa) <1μs L2 Redis IDistributedCache TTL: 10-30dk (uzun) <1ms Database EF Core / SQL 5-100ms HIT → dön MISS MISS ④ write-back 🛡️ Built-in Stampede Protection Aynı key için N eşzamanlı istek → sadece 1 factory çalışır, diğerleri bekler ve sonucu paylaşır SemaphoreSlim per-key (in-process) — manuel lock yazmana gerek yok ✓ L1+L2 otomatik IMemoryCache + Redis ✓ Stampede-proof Tek factory execution ✓ Serialization STJ otomatik ✓ Tag invalidation RemoveByTagAsync()
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 HybridCache tercih 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/Standard tier'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).