HybridCache Derinlemesine: L1/L2, Tag'ler, Logical Invalidation ve Çok Sunuculu Ortamda L1 Tutarsızlığı
RemoveByTagAsync değeri silmez, Redis'e bir zaman damgası yazar ve diğer sunucuların L1'ine haber vermez: iki instance'lı denemede B, L1 süresi 10 saniyeyken 22 saniye sonra bile eski menüyü döndü, çünkü tag damgasını bir kez okuyup saklıyor. Key ile silme + kısa LocalCacheExpiration pencereyi 10 saniyeye indirdi; FusionCache'in Redis backplane'i ise B'yi anında düzeltti.
Sürüm notu: Bu yazıdaki kodlar .NET 10 ile derlenip denenmiştir; örnekler Microsoft.Extensions.Caching.Hybrid 10.11.0, Microsoft.Extensions.Caching.StackExchangeRedis 10.0.14, ZiggyCreatures.FusionCache 2.9.0 ve Redis 8.2 ile koşturuldu.
Önce Tahmin Et
Bookstore'un vitrin API'si (Application Programming Interface) iki instance'ta çalışıyor: A 17521, B 17522 portunda. İkisi de aynı Redis'i L2 (level 2, process dışı ortak cache) olarak kullanıyor, her birinin kendi belleğinde de bir L1'i (level 1, process içi cache) var. Kategori menüsü HybridCache'te category-menu tag'iyle duruyor; Redis'teki süresi 30 dakika. Admin, A üzerinden "Fiction" kategorisinin adını "Novels" yapıyor ve endpoint RemoveByTagAsync("category-menu") çağırıyor.
İki soru. Birincisi: L1 süresi de 30 dakikayken, değişiklikten hemen sonra B'ye gelen istek hangi adı döner? İkincisi: L1 süresini (LocalCacheExpiration) 10 saniyeye indirdik, değişiklikten sonra 22 saniye bekledik ve bu arada A'ya hiç istek gelmedi. B şimdi hangi adı döner?
Expiration, LocalCacheExpiration ve Varsayılanlar
ASP.NET Core Web API serisinin ".NET 9 Yenilikleri: ASP.NET Core'da Yerleşik OpenAPI, Scalar ve HybridCache" yazısı GetOrCreateAsync'i, stampede korumasını ve iki katmanı tanıttı; serinin birinci yazısı da menüyü bu API ile cache'leyip ölçtü. Burada temeli tekrar etmeden ayarların gerçekte nasıl çözüldüğüne bakıyoruz. Uygulamanın kayıt kısmı kısa:
builder.Services.AddStackExchangeRedisCache(o =>
{
o.Configuration = redis;
o.InstanceName = "bookstore:";
});
builder.Services.AddHybridCache();
HybridCacheEntryOptions menuOptions = new()
{
Expiration = expiration, // L2'deki süre; örnekte 30 dakika
LocalCacheExpiration = local // L1'deki süre; komut satırından geliyor
};
app.MapGet("/api/categories/menu", async (CatalogDb db, HybridCache cache, FactoryCounter counter, CancellationToken ct) =>
{
CategoryMenu menu = await cache.GetOrCreateAsync(
"v1:category-menu",
async token =>
{
counter.Increment();
return await MenuQuery.LoadAsync(db, instance, token);
},
menuOptions,
tags: ["category-menu"],
cancellationToken: ct);
return new { servedBy = instance, menu.BuiltBy, first = menu.Items[0].Name, factoryCalls = counter.Calls };
});
Yanıtta iki iz var: servedBy isteği hangi instance'ın karşıladığını, BuiltBy ise menüyü hangi instance'ın factory'sinin ürettiğini gösteriyor. Bir de instance başına factory sayacı. Bu üç bilgiyle cache'in hangi katmandan cevap verdiğini dışarıdan okuyabiliyoruz.
Süreler hakkında üç kural DefaultHybridCache'in kaynak kodunda açıkça yazıyor. Hiçbir şey ayarlamazsan Expiration 5 dakika. LocalCacheExpiration, Expiration'dan uzun verilirse Expiration'a kısaltılıyor; L1 hiçbir zaman L2'den uzun yaşamıyor. Üçüncüsü en çok şaşırtanı: entry'ye özel option verdiğinde bu nesne DefaultEntryOptions ile property property birleştirilmiyor. Yalnız Expiration verirsen L1 süresi de o oluyor. Küçük bir console uygulamasında, varsayılanları "Redis'te 1 saat, bellekte 1 saniye" yapıp denedik:
services.AddHybridCache(o =>
{
o.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromHours(1),
LocalCacheExpiration = TimeSpan.FromSeconds(1)
};
});
// entry'ye özel option'lar varsayılanlarla birleşmez, onların yerine geçer
int defaults = 0, custom = 0;
HybridCacheEntryOptions onlyExpiration = new() { Expiration = TimeSpan.FromHours(1) };
for (int i = 0; i < 2; i++)
{
await cache.GetOrCreateAsync("with-defaults", _ => { defaults++; return ValueTask.FromResult(1); });
await cache.GetOrCreateAsync("with-custom", _ => { custom++; return ValueTask.FromResult(1); }, onlyExpiration);
await Task.Delay(1500);
}
Console.WriteLine($"factory calls after 3 s: defaults={defaults}, custom={custom}");
factory calls after 3 s: defaults=2, custom=1
Varsayılanı kullanan key 1,5 saniye sonra L1'den düştü ve factory yeniden çalıştı. Yalnız Expiration verilen key ise bir saatlik L1 süresi aldı. Bir ekip arkadaşın "L1'i global olarak 1 saniyeye çektim" dediğinde, entry'ye özel option veren her çağrının bu kuralın dışında kaldığını bil.
Tek tek davranışı kapatmak için HybridCacheEntryFlags var. En çok işe yarayanlar:
| Flag | Ne yapar | Ne zaman |
|---|---|---|
DisableLocalCache | L1'e okuma ve yazmayı kapatır | Instance'lar arasında hiç tutarsızlık kabul edilmeyen veri |
DisableDistributedCache | L2'ye okuma ve yazmayı kapatır | Büyük ama instance'a özgü, paylaşmaya değmeyen veri |
DisableUnderlyingData | Factory'yi çağırmaz, cache'te yoksa varsayılan değer döner | "Varsa cache'ten ver, yoksa hesaplama" okumaları |
İki sınır da sessiz çalışıyor. MaximumPayloadBytes varsayılan 1 MB (megabyte), MaximumKeyLength varsayılan 1024 karakter; sınırı aşan payload ya da key loglanıyor ama cache'lenmiyor. Kullanıcının arama metnini key'e koyan bir endpoint'i taklit ettik:
int longKeyCalls = 0;
string longKey = "v1:search:" + new string('x', 1100);
for (int i = 0; i < 2; i++)
await cache.GetOrCreateAsync(longKey, _ => { longKeyCalls++; return ValueTask.FromResult(1); });
Console.WriteLine($"long key factory calls: {longKeyCalls}");
long key factory calls: 2
fail: Microsoft.Extensions.Caching.Hybrid.HybridCache[5] Cache key maximum length exceeded (maximum: 1024, actual: 1110).
fail: Microsoft.Extensions.Caching.Hybrid.HybridCache[5] Cache key maximum length exceeded (maximum: 1024, actual: 1110).
Exception yok, istek başarılı; sadece cache hiç çalışmıyor. Log seviyesi Error olduğu için bu satırı alarma bağlamak kolay, yeter ki birisi baksın. Uzun ve kullanıcıdan gelen girdiyi key'e koyma; hash'le ya da hiç cache'leme.
Instance Reuse: sealed ve [ImmutableObject(true)]
L1 sıradan bir IMemoryCache değil. HybridCache, değiştirilebilir bir tip için her çağrıda serialize edilmiş byte'lardan yeni bir kopya üretiyor, çünkü çağıranlardan biri object'i değiştirirse diğerleri bunu görmemeli. Tip sealed ve [ImmutableObject(true)] ile işaretliyse aynı instance'ı herkese verebiliyor:
// sealed + [ImmutableObject(true)]: HybridCache aynı instance'ı her çağırana verebilir
[ImmutableObject(true)]
public sealed record CategoryMenu(IReadOnlyList<MenuItem> Items, string BuiltBy, DateTimeOffset BuiltAt);
FrozenMenu f1 = await cache.GetOrCreateAsync("frozen", _ => ValueTask.FromResult(new FrozenMenu("Fiction")));
FrozenMenu f2 = await cache.GetOrCreateAsync("frozen", _ => ValueTask.FromResult(new FrozenMenu("Fiction")));
PlainMenu p1 = await cache.GetOrCreateAsync("plain", _ => ValueTask.FromResult(new PlainMenu("Fiction")));
PlainMenu p2 = await cache.GetOrCreateAsync("plain", _ => ValueTask.FromResult(new PlainMenu("Fiction")));
Console.WriteLine($"FrozenMenu same instance: {ReferenceEquals(f1, f2)}");
Console.WriteLine($"PlainMenu same instance: {ReferenceEquals(p1, p2)}");
FrozenMenu same instance: True
PlainMenu same instance: False
PlainMenu de bir record, yani pratikte değiştirilemez; ama HybridCache bunu bilemez ve her L1 hit'te deserialize eder. Sık okunan, büyük bir menüde bu, her istekte boşa harcanan CPU (Central Processing Unit) ve allocation demek. Bedeli de var: attribute bir sözdür. İşaretlediğin tipin içindeki bir List'e sonradan eleman eklersen, aynı instance'ı tutan bütün istekler bunu görür.
Tag'ler: Silme Değil, Zaman Damgası
RemoveByTagAsync bir değeri fiziksel olarak silmiyor. Microsoft Learn bunu "bu andan önce yaratılanı yok say" diye tarif ediyor; kaynak kodda karşılığı basit: tag için o anın zaman damgası hem process içindeki bir sözlüğe hem L2'ye yazılıyor. Her okuma, entry'nin yaratılma zamanını entry'nin tag'lerinin zaman damgasıyla kıyaslıyor ve eski olanı geçersiz sayıyor. Admin endpoint'imiz:
// Menüyü değiştiren tek yol: önce veritabanı, sonra invalidation (?by=key ya da varsayılan tag)
app.MapPut("/api/admin/categories/{id:int}", async (int id, string? by, RenameCategory body, CatalogDb db, HybridCache cache, CancellationToken ct) =>
{
int updated = await db.Categories.Where(c => c.Id == id)
.ExecuteUpdateAsync(s => s.SetProperty(c => c.Name, body.Name), ct);
if (updated == 0) return Results.NotFound();
if (by == "key")
await cache.RemoveAsync("v1:category-menu", ct);
else
await cache.RemoveByTagAsync("category-menu", ct);
return Results.NoContent();
});
Redis'te ne olduğuna invalidation'dan önce ve sonra baktık:
$ docker exec chwork-redis-b redis-cli --scan
bookstore:v1:category-menu
$ curl -s -X PUT localhost:17521/api/admin/categories/1 -H 'Content-Type: application/json' -d '{"name":"Novels"}' -w '%{http_code}'
204
$ docker exec chwork-redis-b redis-cli --scan
bookstore:__MSFT_HCT__category-menu
bookstore:v1:category-menu
$ docker exec chwork-redis-b redis-cli TTL bookstore:v1:category-menu
1800
$ docker exec chwork-redis-b redis-cli TTL bookstore:__MSFT_HCT__category-menu
86400000
Menü key'i yerinde, 1800 saniyelik ömrünün başında. Yanına bir tag kaydı eklendi ve onun TTL'i (Time to Live) 86.400.000 saniye, yani kaynak koddaki sabit olan 1000 gün. Bu tasarımın güzel tarafı, bir tag'e bağlı on bin entry'yi tek bir küçük yazmayla geçersiz kılması. Bedeli ise iki tane: geçersiz değerler Redis'te ve L1'de süreleri dolana kadar yer kaplamaya devam ediyor, ve her instance'ın bu zaman damgasını bir şekilde öğrenmesi gerekiyor. RemoveByTagAsync("*") da aynı mekanizmayla çalışıyor; tek farkı damganın her tag'e uygulanması.
İki Instance, Tek Redis: L1 Tutarsızlığını Görmek
İki instance'ı aynı build'den, farklı port ve isimle başlattık:
$ dotnet Ch04.Api.dll --urls http://localhost:17521 --Instance A --Cache:LocalSeconds 1800
$ dotnet Ch04.Api.dll --urls http://localhost:17522 --Instance B --Cache:LocalSeconds 1800
$ curl -s localhost:17521/api/categories/menu
{"servedBy":"A","builtBy":"A","first":"Fiction","factoryCalls":1}
$ curl -s localhost:17522/api/categories/menu
{"servedBy":"B","builtBy":"A","first":"Fiction","factoryCalls":0}
$ curl -s -X PUT localhost:17521/api/admin/categories/1 -H 'Content-Type: application/json' -d '{"name":"Novels"}' -w '%{http_code}'
204
$ curl -s localhost:17522/api/categories/menu
{"servedBy":"B","builtBy":"A","first":"Fiction","factoryCalls":0}
$ curl -s localhost:17521/api/categories/menu
{"servedBy":"A","builtBy":"A","first":"Novels","factoryCalls":2}
$ curl -s localhost:17522/api/categories/menu
{"servedBy":"B","builtBy":"A","first":"Fiction","factoryCalls":0}
İlk iki satır L2'nin işini gösteriyor: B menüyü hiç hesaplamadı, A'nın Redis'e yazdığı kopyayı aldı. Invalidation'dan sonra A factory'yi yeniden çalıştırdı ve "Novels" döndü; B ise 30 dakika boyunca "Fiction" dönmeye devam edecek. Birinci tahminin cevabı bu ve Microsoft Learn de açıkça söylüyor: key ya da tag ile invalidation o anki sunucuyu ve L2'yi etkiler, diğer sunuculardaki bellek cache'ini etkilemez.

Kısa LocalCacheExpiration Yeter mi?
İlk akla gelen çare L1'i kısaltmak: B'nin kopyası 10 saniyede ölsün, sonra Redis'ten tazesini alsın. Aynı senaryoyu --Cache:LocalSeconds 10 ile iki kez koşturduk. Önce key ile silme (?by=key):
$ curl -s -X PUT 'localhost:17521/api/admin/categories/1?by=key' -H 'Content-Type: application/json' -d '{"name":"Novels"}' -w '%{http_code}'
204
$ curl -s localhost:17522/api/categories/menu
{"servedBy":"B","builtBy":"A","first":"Fiction","factoryCalls":0}
$ sleep 11
$ curl -s localhost:17522/api/categories/menu
{"servedBy":"B","builtBy":"B","first":"Novels","factoryCalls":1}
Beklendiği gibi: B'nin L1'i doldu, Redis'te key yoktu, B kendi factory'sini çalıştırdı ve "Novels" döndü. Tutarsızlık penceresi en fazla 10 saniye. Şimdi aynı şey tag ile:
$ curl -s -X PUT localhost:17521/api/admin/categories/1 -H 'Content-Type: application/json' -d '{"name":"Novels"}' -w '%{http_code}'
204
$ sleep 11
$ curl -s localhost:17522/api/categories/menu
{"servedBy":"B","builtBy":"A","first":"Fiction","factoryCalls":0}
$ sleep 11
$ curl -s localhost:17522/api/categories/menu
{"servedBy":"B","builtBy":"A","first":"Fiction","factoryCalls":0}
$ curl -s localhost:17521/api/categories/menu
{"servedBy":"A","builtBy":"A","first":"Novels","factoryCalls":2}
$ sleep 11
$ curl -s localhost:17522/api/categories/menu
{"servedBy":"B","builtBy":"A","first":"Novels","factoryCalls":0}
İkinci tahminin cevabı: 22 saniye sonra bile "Fiction". B'nin L1'i iki kez doldu ama B, Redis'teki eski payload'ı her seferinde geçerli saydı. Ancak A'ya bir istek gelip A menüyü yeniden üretince ve Redis'teki kopyanın üstüne yazınca B düzeldi.
Sebep kaynak kodda, DefaultHybridCache.TagInvalidation.cs dosyasında. Her instance tag zaman damgalarını bir ConcurrentDictionary<string, Task<long>> içinde tutuyor ve bir tag'i Redis'ten yalnız ilk gördüğünde okuyor; o kaydın bir süresi yok, bir daha okunmuyor. B menüyü ilk aldığında category-menu için "hiç invalid edilmedi" (0) değerini okudu ve sakladı. A'nın sonradan Redis'e yazdığı damgayı hiç görmedi, Redis'teki eski payload'ın yaratılma zamanını 0 ile kıyasladı. "*" için durum daha da katı: global damga yalnız constructor'da okunuyor. Denedik, A'da RemoveByTagAsync("*") çağırıp 11 saniye bekledikten sonra B yine "Fiction" döndü; B'yi yeniden başlattığımızda ise ilk istekte factory'yi çalıştırıp "Novels" döndü. dotnet/extensions deposunda bu kayıtlara bir süre eklenmesini öneren açık bir issue var; o gelene kadar bu davranışı tasarımına dahil et.
Düz HybridCache ile pratik sonuç şu: tutarsızlık penceresini kısaltmak istiyorsan, key'ini bildiğin entry'leri tag'le birlikte RemoveAsync ile de sil ve LocalCacheExpiration'ı kabul edebileceğin eskiliğe çek. Tag'i, key'ini bilmediğin geniş gruplar için kullan ve o gruplarda eskiliğin üst sınırının L2'deki Expiration olduğunu kabul et.
Backplane: FusionCache ile Aynı Kod
Kalıcı çözüm, bir instance'taki değişikliği diğerlerine anında haber veren bir kanal: backplane. Microsoft'un implementasyonunda yok; FusionCache'te Redis pub/sub üzerinde hazır geliyor. Üstelik FusionCache kendini HybridCache olarak kaydedebiliyor, yani endpoint'lere dokunmadan yalnız kayıt kısmını değiştiriyoruz:
builder.Services.AddFusionCache()
.WithDefaultEntryOptions(new FusionCacheEntryOptions
{
IsFailSafeEnabled = true,
FailSafeMaxDuration = TimeSpan.FromHours(2),
FailSafeThrottleDuration = TimeSpan.FromSeconds(30),
FactorySoftTimeout = TimeSpan.FromMilliseconds(500)
})
.WithSerializer(new FusionCacheSystemTextJsonSerializer(new JsonSerializerOptions { IncludeFields = true }))
.WithDistributedCache(new RedisCache(new RedisCacheOptions { Configuration = redis }))
.WithBackplane(new RedisBackplane(new RedisBackplaneOptions { Configuration = redis }))
.AsHybridCache();
Aynı senaryo, L1 süresi yine 30 dakika:
$ docker exec chwork-redis-b redis-cli PUBSUB CHANNELS
__Booksleeve_MasterChanged
FusionCache.Backplane:v2
$ curl -s -X PUT localhost:17521/api/admin/categories/1 -H 'Content-Type: application/json' -d '{"name":"Novels"}' -w '%{http_code}'
204
$ curl -s localhost:17522/api/categories/menu
{"servedBy":"B","builtBy":"B","first":"Novels","factoryCalls":1}
$ curl -s localhost:17521/api/categories/menu
{"servedBy":"A","builtBy":"B","first":"Novels","factoryCalls":1}
$ docker exec chwork-redis-b redis-cli --scan
v2:v1:category-menu
v2:__fc:t:category-menu
İlk kanal StackExchange.Redis'in kendi kanalı, ikincisi backplane. B, A'nın yayınladığı tag mesajıyla L1'indeki kopyayı geçersiz saydı, factory'sini çalıştırdı ve "Novels" döndü. A'nın sonraki cevabındaki "builtBy":"B" da ilginç: B yeni değeri yazınca backplane bu kez A'ya haber verdi, A da L2'den B'nin ürettiği kopyayı aldı. Tag kaydının FusionCache'teki adı __fc:t: ile başlıyor; mekanizma aynı, fark diğer instance'lara mesaj gitmesi.
Kayıttaki iki ayrıntı da FusionCache'ten geliyor. İlk denemede IncludeFields olmadan başlattığımızda şu uyarıyı aldık: "it looks like you are using a serializer based on System.Text.Json not correctly configured to handle value tuples". Uyarı, value tuple'ların field'larının varsayılan System.Text.Json ayarlarıyla serialize edilmemesinden kaynaklanıyor; IncludeFields = true ile kayboldu. İkincisi fail-safe. Menünün Redis süresini 5 saniyeye indirip tabloyu bozduk ve aynı isteği iki implementasyona attık:
$ sqlite3 bookstore.db 'ALTER TABLE Categories RENAME TO Categories_broken'
$ sleep 6
$ curl -s localhost:17521/api/categories/menu -w ' %{http_code}'
500
$ sqlite3 bookstore.db 'ALTER TABLE Categories RENAME TO Categories_broken'
$ sleep 6
$ curl -s localhost:17521/api/categories/menu -w ' %{http_code}'
{"servedBy":"A","builtBy":"A","first":"Fiction","factoryCalls":2} 200
warn: ZiggyCreatures.Caching.Fusion.FusionCache[0]
FUSION [N=FusionCache I=0HNPT7MU313PR] (O=0HNPT7MU313Q2 K=v1:category-menu): FAIL-SAFE activated (from memory)
İlki Microsoft'un HybridCache'i: süresi dolmuş değer gitti, factory exception fırlattı, HTTP (Hypertext Transfer Protocol) 500. İkincisi FusionCache: factory yine çalıştı ve hata verdi (sayaç 2), ama süresi dolmuş değer FailSafeMaxDuration boyunca saklandığı için o döndü. FactorySoftTimeout da benzer bir iş yapıyor: dokümana göre factory 500 milisaniyeden uzun sürer ve elde eski bir değer varsa eski değer hemen dönüyor, factory arka planda bitiyor. Bu ikisini serinin sekizinci yazısında, Redis ve veritabanı düşerken uygulamanın ayakta kalması bağlamında ayrıntılı ölçeceğiz.
Üretimde Dikkat
L1 süresini bir tutarsızlık sözü olarak yaz. "Menü değiştikten sonra bir instance en fazla 10 saniye eski menü gösterebilir" cümlesi, backplane'siz bir kurulumda LocalCacheExpiration'ın gerçek anlamı; tag kullanıyorsan bu cümle "L2 süresine kadar" olur. Kısaltmanın bedeli Redis'e daha sık gitmek: her L1 dolumunda bir ağ turu ve bir deserialization. Menü gibi küçük bir payload'da bu ihmal edilebilir, büyük payload'larda ise ölçmeden karar verme.
Tag okumasının da bir hata davranışı var. Kaynak kodda Redis'ten tag damgası okuma işlemi 4 saniyelik bir timeout'la yapılıyor ve okuma başarısız olursa tag "şimdi invalid edildi" sayılıyor. Yani Redis yavaşladığında tag'li entry'ler güvenli tarafta kalıp factory'ye düşer; veritabanı yükünün aynı anda artacağını hesaba kat.
Backplane'in de sınırları var. Redis pub/sub mesajları kalıcı değil, bağlantısı kopmuş bir instance o arada gelen mesajı kaçırır. FusionCache dokümanı bunun için auto-recovery mekanizmasını anlatıyor, ama ölçmeden güvenme: bir instance'ın Redis bağlantısını koparıp geri getirdiğin bir test, prod'a çıkmadan önce yapılacak ilk iş. Her instance aynı veriyi kendi belleğinde tuttuğu için bellek maliyeti instance sayısıyla çarpılıyor; L1'i yalnız gerçekten sıcak key'ler için kullan.
Sık Düşülen Tuzaklar
RemoveByTagAsync'in Redis'te ve bellekte yer açtığını sanmak. Değerler süreleri dolana kadar yaşıyor; bir tag'le milyonlarca entry'yi invalid etmek bellek baskısını hemen azaltmaz.
Entry'ye özel option verip DefaultEntryOptions'taki LocalCacheExpiration'ın geçerli olacağını sanmak. Yalnız Expiration verirsen L1 de o kadar yaşar; ikisini birlikte ver.
Tipi [ImmutableObject(true)] ile işaretleyip sonra değiştirmek. Attribute bir kontrol değil, bir söz; instance artık tüm isteklerin ortak malı.
AddDistributedMemoryCache'i L2 sanmak. HybridCache constructor'ı bu kaydı görünce L2'yi bilinçli olarak devre dışı bırakıyor; tek instance'ta zaten fark yok, çok instance'ta ise ortak bir L2'n yok demek.
Uzun ya da sınırsız key üretmek. 1024 karakteri aşan key hata fırlatmıyor, sessizce cache'lenmiyor; tek iz Error seviyesindeki log satırı.
Çok instance'lı bir ortamda RemoveByTagAsync("*")'ı "her yeri temizle" butonu sanmak. Denememizde diğer instance bu damgayı yeniden başlatılana kadar görmedi.
Küçük Bir Deneme
★☆☆ İki instance'ı --Cache:LocalSeconds 5 ile başlat ve ?by=key ile ad değiştir. B'ye her saniye istek atan bir döngüyle tutarsızlık penceresini ölç; 5 saniyeyi aşıyor mu?
★★☆ Admin endpoint'ini, RemoveByTagAsync'ten hemen sonra menüyü GetOrCreateAsync ile yeniden üretecek şekilde değiştir. Tag senaryosunda B ne zaman "Novels" dönmeye başlıyor? Bu "invalidation sonrası ısıtma" yaklaşımının key'ini bilmediğin entry'lerde neden işe yaramadığını açıkla.
★★★ FusionCache modunda factory'ye 2 saniyelik bir Task.Delay ekle, menünün Redis süresini 5 saniyeye indir ve süresi dolduktan sonraki ilk isteğin süresini curl -w '%{time_total}' ile ölç. FactorySoftTimeout olmadan ve onunla sonuç nasıl değişiyor?
Bu yazıda uygulama içindeki iki katmanın tutarlılık bedelini ölçtük: tag'ler ucuz ama mantıksal, L1 ise backplane olmadan her instance'ın kendi gerçeği. Bir sonraki yazıda cache'i sunucudan çıkarıp istemciye ve aradaki proxy'lere taşıyoruz: Cache-Control, ETag, 304 Not Modified ve Vary ile isteğin sunucuya hiç gelmemesini ya da gövdesiz dönmesini sağlamak.