Mert Özen Her Satırda Daha İleri
Backend Caching — #03

IDistributedCache ve Redis'i Cache Olarak İşletmek: Serialization, Key Tasarımı, TTL ve Eviction Policy

Mert Özen 8 Kas 2026 13 dk 5 görüntülenme
IDistributedCache ve Redis'i Cache Olarak İşletmek: Serialization, Key Tasarımı, TTL ve Eviction Policy

Redis'i cache olarak işletmek tek ConnectionMultiplexer, kısa timeout, sürümlü key ve allkeys-lru ister. Varsayılan ayarlarla Redis durunca her istek 11-12 sn sürdü; WaitAsync ile 200 ms sınır bunu 0,4 sn'ye, basit bir circuit breaker 1 ms'nin altına indirdi. Resmi redis:8.2 image'ı noeviction ile geliyor: 8 MB dolunca yazmalar OOM hatası aldı, allkeys-lru 2.534 key atarak devam etti. Redis 8 AGPLv3 seçeneği sunuyor; Valkey BSD, Garnet MIT lisanslı.

Sürüm notu: Bu yazıdaki kodlar .NET 10 ile derlenip denenmiştir; örnekler Redis 8.2 (8.2.11), Microsoft.Extensions.Caching.StackExchangeRedis 10.0.14, StackExchange.Redis 3.4.31 ve MessagePack 3.1.12 ile koşturuldu.

Önce Tahmin Et

Bookstore API'sinin (Application Programming Interface) kitap detayı endpoint'i cache-aside çalışıyor: önce Redis'e bakıyor, yoksa SQLite'tan okuyup Redis'e yazıyor. Redis çağrıları bir try/catch içinde; hata olursa cache'i atlayıp veritabanından cevap veriyor. Connection string'de yalnız localhost:17511 var, StackExchange.Redis'in hiçbir ayarına dokunulmamış.

İki soru. Birincisi: Redis container'ı durdu. Bir istek kaç saniyede döner? İkincisi: resmi redis:8.2 image'ında maxmemory'yi 8 MB yapıp içine 10 dakika TTL'li (Time to Live) kitaplar yazmaya devam ediyorsun. Bellek dolunca ne olur?

Nereden Devam Ediyoruz

ASP.NET Core Web API serisinin "ASP.NET Core Web API'de Caching: IMemoryCache, Redis ve Response Caching" yazısı AddStackExchangeRedisCache, InstanceName ve GetStringAsync/SetStringAsync ile Redis'e ilk bakışı yaptı. System Design serisinin "Caching Stratejileri: Cache-Aside, Write-Through, Write-Behind ve Cache Stampede" yazısı da LRU (Least Recently Used) ile LFU (Least Frequently Used) farkını ve Redis'in varsayılan policy'sinin noeviction olduğunu söyledi. Bu yazı o temelin üstüne, Redis'i üretimde cache olarak işletmenin .NET tarafını kuruyor: bağlantı, serialization, key tasarımı, bellek sınırı ve Redis yokken ayakta kalmak. Redis'in veri yapıları, persistence, cluster ve pub/sub ilerideki Veritabanı › Redis serisinin konusu; burada Redis yalnız bir cache.

Tek Bir ConnectionMultiplexer ve StackExchange.Redis 3.x

İlk sürpriz paket ağacında. Microsoft.Extensions.Caching.StackExchangeRedis 10.0.14'ü eklediğinde gelen istemci 3.x değil:

$ dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis --version 10.0.14
$ dotnet list package --include-transitive | grep StackExchange.Redis
   > StackExchange.Redis               2.7.27
$ dotnet add package StackExchange.Redis --version 3.4.31
$ dotnet list package --include-transitive | grep StackExchange.Redis
   > StackExchange.Redis                                  3.4.31      3.4.31

Paket bir alt sınır belirtiyor ve NuGet o sınırı seçiyor. StackExchange.Redis 3.0 Haziran 2026'da çıktı; GitHub sürüm notlarına göre public API'yi bilerek değiştirmiyor, ağ katmanını ve parser'ı baştan yazıyor ve varsayılan protokolü RESP3 (Redis Serialization Protocol 3) yapıyor. Tek davranış farkı, AllowAdmin kapalıyken Execute("FLUSHALL") gibi admin komutlarının artık istemcide reddedilmesi. Yeni sürümü istiyorsan paketi doğrudan referans et.

İkinci konu bağlantının kendisi. ConnectionMultiplexer pahalı bir object: TCP (Transmission Control Protocol) bağlantılarını, yeniden bağlanma mantığını ve komut kuyruğunu yönetir; uygulama başına bir tane olmalı. AddStackExchangeRedisCache kendi multiplexer'ını açar. Uygulamanın başka bir yerinde de Redis'i doğrudan kullanıyorsan ikinci bir bağlantı havuzu açmamak için tek multiplexer'ı paylaştır:

ConfigurationOptions redis = ConfigurationOptions.Parse(builder.Configuration.GetConnectionString("Redis")!);
redis.AbortOnConnectFail = false; // factory verdiğimizde RedisCache bunu bizim yerimize ayarlamıyor
IConnectionMultiplexer mux = await ConnectionMultiplexer.ConnectAsync(redis);

builder.Services.AddSingleton(mux); // uygulamadaki tek multiplexer; doğrudan Redis kullanan kod da bunu alır
builder.Services.AddStackExchangeRedisCache(o =>
{
    o.InstanceName = "bookstore:";
    o.ConnectionMultiplexerFactory = () => Task.FromResult(mux);
});

Yorumdaki ayrıntı önemli. RedisCache kaynak kodunda, connection string'den kendisi bağlanırken AbortOnConnectFail = false ayarlıyor; yorumu da açık: başlangıçta erişilemeyen bir sunucu DI (Dependency Injection) container'ının service'i oluşturmasını engellemesin. Factory verdiğinde bu ayar senin sorumluluğunda. Varsayılan true ile Redis kapalıyken uygulama hiç açılmıyor:

Unhandled exception. StackExchange.Redis.RedisConnectionException: It was not possible to connect to the redis server(s). Error connecting right now. To allow this multiplexer to continue retrying until it's able to connect, use abortConnect=false in your connection string or AbortOnConnectFail=false; in your code.

false ile aynı koşulda API açıldı ve ilk istek veritabanından 200 döndü; multiplexer arka planda bağlanmayı denemeye devam ediyor.

Serialization ve Redis'teki Karşılığı

IDistributedCache byte array saklar; tipini sen serialize edersin. Varsayılan seçim System.Text.Json, ama reflection yerine source generation ile:

[MessagePackObject]
public sealed record BookDetail(
    [property: Key(0)] int Id,
    [property: Key(1)] string Title,
    [property: Key(2)] string Author,
    [property: Key(3)] decimal Price,
    [property: Key(4)] DateOnly PublishedOn,
    [property: Key(5)] string[] Tags,
    [property: Key(6)] string Description);

// derleme zamanında üretilen serializer: reflection yok, trimming/AOT dostu
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
[JsonSerializable(typeof(BookDetail))]
public sealed partial class CacheJsonContext : JsonSerializerContext;

Aynı kitabı üç yolla serialize edip 100.000 serialize/deserialize turunu ölçtük:

System.Text.Json (reflection): 499 bytes
System.Text.Json (source gen): 499 bytes
MessagePack                  : 413 bytes
STJ source gen  100k round trips: 217 ms
MessagePack     100k round trips: 142 ms

Source generation boyutu değiştirmiyor; kazancı başlangıçta reflection metadata'sı kurmamak ve trimming ile Native AOT'ye (Ahead-of-Time) uyum. MessagePack binary olduğu için yüzde 17 daha küçük ve bu makinede yaklaşık 1,5 kat hızlı. Bedeli okunabilirlik ve şema disiplini: redis-cli ile JSON'a (JavaScript Object Notation) bakıp hatayı anlarsın, MessagePack'e bakamazsın; Key(n) numaralarını değiştirirsen eski entry'ler yanlış alanlara okunur. Çoğu cache için JSON yeterli; payload büyük ve CPU (Central Processing Unit) maliyeti ölçülebilir hâle geldiğinde MessagePack'e geç.

Redis tarafında entry'nin neye dönüştüğüne bakalım (redis-cli'yi container içinde docker exec ile çalıştırıyoruz):

$ redis-cli --scan --pattern 'bookstore:*'
bookstore:v2:book:42:msgpack
bookstore:v2:book:42
$ redis-cli TYPE bookstore:v2:book:42
hash
$ redis-cli HGETALL bookstore:v2:book:42
absexp
639297017962054830
sldexp
-1
data
{"id":42,"title":"The Pragmatic Bookstore","author":"Ada Lovelace","price":39.90,"publishedOn":"2024-05-14","t
$ redis-cli TTL bookstore:v2:book:42
593
$ redis-cli OBJECT ENCODING bookstore:v2:book:42
hashtable
$ redis-cli CONFIG GET hash-max-listpack-value
hash-max-listpack-value
64
$ redis-cli MEMORY USAGE bookstore:v2:book:42
776
$ redis-cli MEMORY USAGE bookstore:v2:book:42:msgpack
720

RedisCache her entry'yi üç alanlı bir hash olarak yazıyor: .NET tick cinsinden absolute expiration, sliding expiration ve veri. 499 byte'lık JSON Redis'te 776 byte tutuyor; alan 64 byte'tan uzun olduğu için hash kompakt listpack yerine hashtable olarak saklanıyor. Kaynakta iki ayrıntı daha var. Yazma, HSET ve EXPIRE komutlarını bir batch ile pipeline ediyor; bu bir MULTI/EXEC transaction'ı değil, bağlantı tam arada koparsa TTL'siz bir key kalabilir. Sliding expiration kullanırsan her okumada ek bir EXPIRE gider; okuma ağırlıklı bir cache'te okuma başına iki komut demek. Absolute expiration çoğu zaman daha doğru seçim.

Key Tasarımı ve Versiyonlama

Key'in her parçasının bir görevi var: bookstore:v2:book:42.

bookstore: InstanceName'den geliyor ve aynı Redis'i paylaşan uygulamaları ayırıyor. v2 şema sürümü: BookDetail'e alan eklediğinde ya da MessagePack'e geçtiğinde sürümü artırırsın, yeni kod eski formatı hiç okumaz, deploy sırasında eski ve yeni instance'lar birbirinin entry'sini bozmaz ve eski key'ler TTL ile kendiliğinden ölür. book:42 ise varlığın kimliği. Bu düzen ayrıca SCAN ile bir sürümü ya da bir varlık tipini incelemeyi kolaylaştırıyor. KEYS * kullanma; bütün keyspace'i tek komutta tarar ve o sırada Redis başka hiçbir komutu işlemez.

Key'i kısa ama okunur tut ve içine kullanıcı girdisini koymadan önce normalize et; serinin ikinci yazısında, sınırsız key üretiminin IMemoryCache'i nasıl şişirdiğini görmüştük ve aynı sorun Redis'te bütün instance'ların ortak belleğini tüketir.

maxmemory ve Eviction Policy

Resmi image'ın varsayılanları:

$ redis-cli CONFIG GET maxmemory-policy
maxmemory-policy
noeviction
$ redis-cli CONFIG GET maxmemory
maxmemory
0

maxmemory 0 64 bit sistemlerde sınırsız demek; Redis makinenin belleğini bitirene kadar büyür. Sınırı 8 MB yapıp aynı yükü üç policy ile verdik. Her entry 2.000 karakterlik açıklaması olan bir kitap:

$ redis-cli CONFIG SET maxmemory 8mb
OK
--- noeviction, TTL 600 s
write 2426 failed: RedisServerException: OOM command not allowed when used memory > 'maxmemory'.
done, keys in db: 2425
--- volatile-lru, no TTL
write 2450 failed: RedisServerException: OOM command not allowed when used memory > 'maxmemory'.
done, keys in db: 2449
--- allkeys-lru, no TTL
done, keys in db: 2479
$ redis-cli INFO stats | grep -E "^(evicted_keys|expired_keys)"
expired_keys:0
evicted_keys:2534
$ redis-cli INFO memory | grep -E "^(used_memory_human|maxmemory_human|maxmemory_policy)"
used_memory_human:8.00M
maxmemory_human:8.00M
maxmemory_policy:allkeys-lru

Tahminin ikinci cevabı: noeviction'da bellek dolunca her yazma OOM (Out Of Memory) hatası alıyor; okumalar çalışmaya devam ediyor ama cache yeni veri kabul etmiyor. volatile-lru yalnız TTL'si olan key'ler arasından seçer; TTL'siz key'lerle doluysa noeviction gibi davranır. Bu, SetAsync'i options vermeden çağıran tek bir kod yolunun bütün cache'i kilitleyebileceği anlamına geliyor. allkeys-lru ise 2.534 key atarak sınırın içinde kaldı.

PolicyKimi atarCache için
noevictionKimseyi; yazma hata alırUygun değil
allkeys-lruEn uzun süredir erişilmeyen herhangi bir keyİyi varsayılan
allkeys-lfuEn seyrek erişilen keySıcak küme belirgin ve kalıcıysa
volatile-lru / volatile-lfuYalnız TTL'li key'ler arasındanRedis cache dışında da kullanılıyorsa
volatile-ttlKalan TTL'i en kısa olanTTL'ler önem sırasını yansıtıyorsa
allkeys-lrm / volatile-lrmEn uzun süredir yazılmayan (Redis 8.6+)Sık okunan ama güncellenmeyen veriyi ayırt etmek için

Redis dokümanı, özel bir sebebin yoksa allkeys-lru'yu iyi bir varsayılan olarak öneriyor. Redis'in LRU'su yaklaşık bir algoritma: her eviction'da maxmemory-samples (varsayılan 5) kadar key örnekleyip en eskisini atıyor. LRM (Least Recently Modified) policy'leri Redis 8.6 ile geldi; denediğimiz 8.2'de yoklar. Bir gözlem daha: deneylerden sonra container'ı yeniden başlattığımızda CONFIG GET maxmemory-policy tekrar noeviction döndü. CONFIG SET kalıcı değil; ayarı redis.conf'a ya da container komutuna (--maxmemory 512mb --maxmemory-policy allkeys-lru) yaz. Redis'i hem cache hem kalıcı veri için kullanacaksan dokümanın önerisi iki ayrı instance.

Redis Düşerse

Cache bir hızlandırıcı, bağımlılık değil. Redis'e erişilemiyorsa istek veritabanından cevap vermeli ve bunu hızlı yapmalı. Kitap detayı endpoint'i Redis'e doğrudan değil, şu sarmalayıcı üzerinden gidiyor:

// Redis bir hızlandırıcı, bağımlılık değil: hata = miss, sonra bir süre Redis'e hiç gitme
public sealed class SafeCache(IDistributedCache cache, SafeCacheOptions options, TimeProvider time, ILogger<SafeCache> logger)
{
    private long _skipUntilTicks;

    public async Task<T?> GetAsync<T>(string key, JsonTypeInfo<T> type, CancellationToken ct) where T : class
    {
        if (IsOpen()) return null;
        try
        {
            // SE.Redis async timeout'u heartbeat'te (saniyede bir) kontrol eder; kesin üst sınırı biz koyuyoruz
            byte[]? bytes = await cache.GetAsync(key, ct).WaitAsync(options.Timeout, ct);
            return bytes is null ? null : JsonSerializer.Deserialize(bytes, type);
        }
        catch (Exception ex) when (ex is RedisException or TimeoutException)
        {
            Trip(ex);
            return null;
        }
    }

    public async Task SetAsync<T>(string key, T value, JsonTypeInfo<T> type, DistributedCacheEntryOptions entry, CancellationToken ct)
    {
        if (IsOpen()) return;
        try
        {
            await cache.SetAsync(key, JsonSerializer.SerializeToUtf8Bytes(value, type), entry, ct).WaitAsync(options.Timeout, ct);
        }
        catch (Exception ex) when (ex is RedisException or TimeoutException)
        {
            Trip(ex);
        }
    }

    private bool IsOpen() => time.GetUtcNow().UtcTicks < Interlocked.Read(ref _skipUntilTicks);

    private void Trip(Exception ex)
    {
        Interlocked.Exchange(ref _skipUntilTicks, (time.GetUtcNow() + options.BreakDuration).UtcTicks);
        logger.LogWarning("Redis unavailable, skipping cache for {Seconds} s: {Error}",
            options.BreakDuration.TotalSeconds, ex.GetType().Name);
    }
}
app.MapGet("/api/books/{id:int}", async (int id, SafeCache cache, CatalogDb db, CancellationToken ct) =>
{
    string key = $"v2:book:{id}";
    BookDetail? book = await cache.GetAsync(key, CacheJsonContext.Default.BookDetail, ct);
    if (book is not null) return Results.Ok(book);

    book = await db.Books.AsNoTracking().Where(b => b.Id == id)
        .Select(b => new BookDetail(b.Id, b.Title, b.Author, b.Price, b.PublishedOn, b.Tags.Split(',', StringSplitOptions.None), b.Description))
        .FirstOrDefaultAsync(ct);
    if (book is null) return Results.NotFound();

    await cache.SetAsync(key, book, CacheJsonContext.Default.BookDetail, bookTtl, ct);
    return Results.Ok(book);
});

Aynı uygulamayı dört ayarla çalıştırdık: Redis ayaktayken bir miss ve bir hit, sonra docker stop, ardından beş istek, sonra docker start ve iki istek. Her ayardan ilgili satırlar:

== defaults
redis up,   hit  status=200 time=0.003260s
redis down, req 1 status=200 time=11.093907s
redis down, req 5 status=200 time=11.984520s
== short
redis down, req 1 status=200 time=1.318846s
redis down, req 5 status=200 time=1.985306s
== waitasync
redis down, req 1 status=200 time=0.409507s
redis down, req 5 status=200 time=0.406430s
== breaker
redis down, req 1 status=200 time=0.205936s
redis down, req 2 status=200 time=0.001142s
redis down, req 5 status=200 time=0.000734s
redis back, req 1 status=200 time=0.001299s

Tahminin ilk cevabı: yaklaşık 11–12 saniye, ve bu her istek için. try/catch'in olması yetmiyor. Varsayılan ConnectTimeout, SyncTimeout ve AsyncTimeout 5.000 ms; bağlantı yokken komutlar backlog'da bağlantının geri gelmesini bekliyor ve süre dolunca RedisConnectionException ile düşüyor, önce okuma sonra yazma. short satırında connection string'e connectTimeout=1000,syncTimeout=250,asyncTimeout=250 ekledik ve süre 2 saniyeye indi, 250 ms'ye değil. Sebebi StackExchange.Redis'in kendi dokümanında: async timeout'lar yalnız heartbeat'te, varsayılan olarak saniyede bir kontrol ediliyor. Doküman async işlemler için WaitAsync önermesi de bundan; waitasync satırında her Redis çağrısını 200 ms ile sınırladık ve istek okuma ile yazmanın toplamı olan 0,4 saniyeye indi. Son adım basit bir circuit breaker: ilk hatadan sonra 15 saniye Redis'e hiç gitmiyoruz; ilk istek 0,2 saniye, sonrakiler 1 ms'nin altında. Redis geri geldiğinde pencere dolana kadar cache'siz devam ediyoruz, sonraki istek Redis'i yeniden deniyor.

Bu breaker bilerek ilkel: tek hata pencereyi açıyor. Eşik, yarı açık durum ve retry gibi ayrıntılar System Design serisinin "Reliability Pattern'leri: Timeout, Retry, Backoff + Jitter, Circuit Breaker ve Load Shedding" yazısında. İki uyarı daha: WaitAsync çağıranı serbest bırakır ama komut yine de sonradan Redis'e gidebilir, ve Redis yokken bütün yük veritabanına biner. SQLite'ta 1 ms tutan sorgu gerçek bir veritabanında yük altında saniyelere çıkabilir; bunun önlemleri serinin sekizinci yazısında.

Lisans: Redis 8, Valkey ve Garnet

Redis'i self-host ediyorsan lisans bir mimari karar. Tabloda geçen kısaltmalar: RSALv2 (Redis Source Available License v2), SSPLv1 (Server Side Public License v1), AGPLv3 (GNU Affero General Public License v3), OSI (Open Source Initiative), BSD (Berkeley Software Distribution), MIT (Massachusetts Institute of Technology) lisansı ve RESP (Redis Serialization Protocol). Durum birincil kaynaklara göre şöyle:

SunucuLisansNot
Redis 7.2.x ve öncesiBSD-3-ClauseKlasik açık kaynak Redis
Redis 7.4.x–7.8.xRSALv2 veya SSPLv1Mart 2024 değişikliği; ikisi de OSI onaylı değil
Redis 8 ve sonrasıRSALv2, SSPLv1 veya AGPLv3Kullanıcı birini seçer; AGPLv3 OSI onaylı
ValkeyBSD 3-ClauseLinux Foundation; Mart 2024'te Redis 7.2.4'ten fork
GarnetMITMicrosoft Research; .NET ile yazılmış, RESP konuşuyor

Redis'in lisans sayfası RSALv2 ve SSPLv1 için açıkça "açık kaynak değil" diyor; AGPLv3 seçeneği 1 Mayıs 2025 tarihli duyuruyla Redis 8'e eklendi. AGPLv3'ün ağ üzerinden sunma maddesi asıl olarak Redis'in kendisini değiştirip hizmet olarak sunanları ilgilendiriyor, ama bağlayıcı yorum hukuk ekibinin işi. Valkey, lisans değişikliğine cevap olarak Linux Foundation altında başladı ve valkey.io'ya göre güncel sürümü 9.1.3. Garnet ise Microsoft Research'ün cache-store'u; RESP konuştuğu için değiştirilmemiş Redis istemcileriyle çalışıyor.

.NET tarafında üçü için de kod aynı: RedisCache ve StackExchange.Redis RESP konuşan sunucuyla çalışır. Bu yazıdaki ölçümler yalnız Redis 8.2 ile alındı; Valkey ya da Garnet'e geçeceksen kendi yükünle ve kullandığın komutlarla doğrula.

Üretimde Dikkat

maxmemory ve maxmemory-policy'yi configuration dosyasında ya da container argümanında ver, CONFIG SET'e güvenme; yeniden başlatmada kaybolduğunu gördük. Sınırı makinenin belleğinin tamamına eşitleme, replikasyon buffer'ları ve fragmentation için pay bırak.

INFO stats içindeki evicted_keys, keyspace_hits ve keyspace_misses'i izle. Eviction sayısı sürekli artıyorsa cache küçük ya da TTL'ler gereğinden uzun; hit ratio'yu ve bu metric'leri dışarı vermeyi serinin dokuzuncu yazısında ele alacağız.

Payload boyutunu ölç. MEMORY USAGE ile birkaç tipik key'e bak; JSON'un üstüne hash yükü biniyor. 100 KB'lık bir entry'yi her istekte okumak ağı ve Redis'i senin sandığından çok yorar.

Timeout'ları uygulamanın gecikme bütçesinden türet. 5 saniyelik varsayılan bir cache için absürt; Redis'ten cevap 200 ms'de gelmiyorsa veritabanına gitmek daha hızlıdır.

Sık Düşülen Tuzaklar

SetAsync'i DistributedCacheEntryOptions vermeden çağırmak. Key TTL'siz yazılır, volatile-* policy'lerde hiç atılmaz ve şema değişse bile sonsuza kadar kalır.

Her istekte yeni ConnectionMultiplexer açmak. Her biri kendi bağlantılarını kurar; yük altında Redis'in bağlantı sınırına ve port tükenmesine gidersin.

Factory ile multiplexer verip AbortOnConnectFail'i unutmak. Redis'in kısa bir kesintisi deploy'u ya da pod yeniden başlatmasını düşürür.

Üretimde KEYS çalıştırmak. Bir debug script'inin KEYS bookstore:* komutu, milyonlarca key'lik bir instance'ta bütün istemcileri bekletir; SCAN kullan.

Redis'i bağımlılık gibi kullanmak. Cache'ten okuyamadığında 500 dönen ya da 12 saniye bekleyen bir endpoint, cache'in neden var olduğunu unutmuş demektir.

Küçük Bir Deneme

★☆☆ Eviction deneyini allkeys-lfu ile tekrarla ve birkaç key'i yazma sırasında tekrar tekrar oku. OBJECT FREQ ile sayaçlara bak; hangi key'ler hayatta kalıyor?

★★☆ SafeCache'e serializer'ı dışarıdan alan bir soyutlama ekle ve 1.000 kitabı önce JSON, sonra MessagePack ile yaz. INFO memory içindeki used_memory_human farkı tek key'lik ölçümle tutarlı mı?

★★★ Redis düşme deneyini docker stop yerine docker pause ile yap. Bu durumda TCP bağlantısı açık kalıyor ama cevap gelmiyor. Dört ayarın süreleri nasıl değişiyor, ve hangi ayar ağ bölünmesine karşı seni gerçekten koruyor?

Bu yazıda Redis'i cache olarak işletmenin .NET tarafını kurduk: tek multiplexer, açık timeout'lar, sürümlü key'ler, doğru maxmemory-policy ve Redis yokken hızlıca veritabanına düşmek. Elimizde iki ayrı katman var: hızlı ama instance'a özel IMemoryCache ve ortak ama ağ maliyeti olan Redis. Bir sonraki yazıda ikisini birleştiren HybridCache'e derinlemesine bakıyoruz: L1 (level 1, process içi) ve L2 (level 2, ortak) katmanları, tag'ler, logical invalidation ve çok sunuculu ortamda L1 tutarsızlığı.