Redis Lab'ı ve StackExchange.Redis: Docker, redis-cli, Redis Insight, ConnectionMultiplexer ve Key Tasarımı
StackExchange.Redis'te ConnectionMultiplexer uygulama başına bir tane olmalı: her multiplexer iki bağlantı açar ve istek başına oluşturulduğunda 200 istek 400 bağlantı ve 10 kat yavaş cevap demek. Gerçek çıktılarla Redis 8 ve Valkey 9'lu compose lab'ı, redis-cli latency modları, okunmayan MONITOR'ün 101 MB bellek tüketmesi, Redis Insight, RESP3 ve ayrı pub/sub bağlantısı, pipelining'in 300 kat farkı, sync-over-async ile 110 saniyelik timeout ve SCAN ile key listeleme.
Sürüm notu: Bu yazıdaki örnekler Redis 8.10.3 (redis:8), Valkey 9.1.3 (valkey/valkey:9), Redis Insight 3.8.0, .NET 10 ve StackExchange.Redis 3.4.35 ile çalıştırılmıştır.
Önce Tahmin Et
Bookstore'un (sitedeki serilerin ortak örnek uygulaması, bir online kitapçı) API'sine kitap görüntülenme sayacı ekleniyor. İlk taslak, Redis'e her istekte yeni bir bağlantı açıp işi bitince kapatıyor; "connection'ı using ile kapatıyoruz, sızıntı yok" diye düşünülmüş:
// Anti-pattern: her "istek" için yeni bir multiplexer
for (int i = 0; i < 200; i++)
{
ConnectionMultiplexer mux = await ConnectionMultiplexer.ConnectAsync(connectionString + ",name=per-request");
await mux.GetDatabase().StringIncrementAsync("bookstore:book:42:views");
if (dispose)
{
await mux.DisposeAsync();
}
else
{
leaked.Add(mux); // kimse dispose etmiyor: bağlantı açık kalıyor
}
}
200 istek için Redis'e kaç yeni TCP bağlantısı gelir: 200 mü? Biri DisposeAsync'i unutursa ne olur? Aynı 200 isteği tek bir paylaşılan multiplexer ile yapınca süre ne kadar değişir?
dotnet run --project src/Bookstore.RedisLab -- mux-per-request
dotnet run --project src/Bookstore.RedisLab -- mux-per-request --leak
dotnet run --project src/Bookstore.RedisLab -- mux-singleton
200 requests: 462 ms, 2.31 ms/request, new connections: 400, connected_clients now: 2
200 requests: 400 ms, 2.00 ms/request, new connections: 400, connected_clients now: 402
200 requests: 44 ms, 0.22 ms/request, new connections: 2
200 değil, 400 bağlantı. Her ConnectionMultiplexer iki bağlantı açıyor: komutlar için bir interactive bağlantı ve pub/sub için ayrı bir bağlantı. Dispose unutulunca istek bitse de bağlantılar kalıyor: connected_clients 402'ye çıkmış (artı ikisi gözlem için açtığım bağlantı). Tek multiplexer ile aynı iş 10 kat hızlı, çünkü her istekte TCP bağlantısı, HELLO handshake ve topology keşfi tekrarlanmıyor. Bu yazıda bunun nedenini ve doğrusunu görüyoruz; önce Bookstore için kalıcı bir Redis lab'ı kuruyoruz.
Lab: Redis 8 ve Valkey 9 Yan Yana
01'deki tek seferlik docker run yerine artık bir compose dosyası kullanıyoruz. Redis ve Valkey aynı anda ayakta; Redis Insight isteğe bağlı bir profile'da:
# Bookstore Redis lab: Redis 8 ve Valkey 9 yan yana, Redis Insight isteğe bağlı
name: bookstore-redis-lab
services:
redis:
image: redis:8
ports:
# yalnız bu makineden erişilebilir
- "127.0.0.1:6379:6379"
volumes:
- redis-data:/data
valkey:
image: valkey/valkey:9
ports:
- "127.0.0.1:6380:6379"
volumes:
- valkey-data:/data
insight:
image: redis/redisinsight:3.8
profiles: ["tools"]
ports:
- "127.0.0.1:5540:5540"
volumes:
- insight-data:/data
volumes:
redis-data:
valkey-data:
insight-data:
# 01'deki geçici container'lar
docker rm -f bookstore-redis bookstore-valkey
docker compose -f lab/compose.yaml up -d
docker compose -f lab/compose.yaml exec redis redis-cli CONFIG GET protected-mode
1) "protected-mode"
2) "no"
Port eşlemesindeki 127.0.0.1 bilerek orada. Resmî redis:8 ve valkey/valkey:9 image'larında protected-mode kapalı ve sunucu bütün arayüzleri dinliyor (bind * -::*); container ağında başka türlü çalışmaz. Şifresiz bir Redis'i -p 6379:6379 ile yayınlarsan makinenin ağındaki herkes ona komut gönderebilir. Lab'da bu yüzden yalnız loopback'e bağlıyoruz; production'daki ACL (Access Control List; kullanıcı bazında komut ve key izinleri) ve TLS (bağlantıyı şifreleyen protokol) ayarları 14'ün konusu. Volume'lar /data'ya bağlı, çünkü image varsayılan RDB (Redis'in belli aralıklarla aldığı snapshot dosyası) ayarıyla (save 3600 1 300 100 60 10000) dump.rdb'yi oraya yazıyor; container silinip yeniden oluşturulsa da veri kalıyor.
redis-cli, MONITOR ve Redis Insight
Windows'ta redis-cli kurmana gerek yok; image'ın içinde var. docker compose -f lab/compose.yaml exec redis redis-cli interactive bir oturum açar, sonuna komut yazarsan tek komut çalıştırıp çıkar. Teşhis için en çok işe yarayan modları latency ölçümleri:
docker compose -f lab/compose.yaml exec redis redis-cli --latency
docker compose -f lab/compose.yaml exec redis redis-cli --latency-history -i 1
docker compose -f lab/compose.yaml exec redis redis-cli --latency --latency-percentiles 50,99,99.9
min: 0.025, max: 0.661, avg: 0.155 (429 samples)
min: 0.076, max: 0.526, avg: 0.184 (91 samples) -- 1.00 seconds range
min: 0.047, max: 0.425, avg: 0.127 (91 samples) -- 1.00 seconds range
min: 0.031, max: 0.887, avg: 0.142 (87 samples) -- 1.01 seconds range
min: 0.029, max: 2.038, avg: 0.172 (424 samples), p50: 0.160, p99: 0.469, p99.9: 2.038
Değerler milisaniye. --latency sürekli PING atıp en düşük, en yüksek ve ortalama süreyi verir; --latency-history aynı şeyi aralıklarla yeni satır açarak yapar, böylece latency'nin zamanla nasıl değiştiğini görürsün. Yüzdelik dilimleri yazdıran --latency-percentiles Redis 8.10 ile geldi. Bir uyarı: redis-cli'yi container'ın içinde çalıştırdığında ölçtüğün şey sunucunun kendi gecikmesi; uygulamanın ağdan gördüğü süre bunun üstüne eklenir. 01'de bu farkı ölçmüştük.
MONITOR ise sunucuya gelen her komutu canlı akıtır ve debug için çok caziptir. Redis dokümanı, tek bir MONITOR client'ının kendi ölçümlerinde throughput'u yarıdan fazla düşürdüğünü yazıyor. Bu makinede tek MONITOR'ün çıktısı hızlıca /dev/null'a akarken redis-benchmark'ın SET sonucu saniyede 292.825'ten 275.103'e düştü; asıl tehlike başka yerde çıktı. MONITOR gönderip okumayı bırakan bir client'la (ağı yavaş bir dizüstü, takılmış bir terminal) 200.000 tane 512 byte'lık SET çalıştırdım:
SET: 198412.69 requests per second, p50=0.215 msec
name=stalled-monitor omem=106292736 tot-mem=106298112 cmd=monitor
used_memory_human:162.35M
mem_clients_normal:106303488
Okunmayan MONITOR çıktısı sunucunun belleğinde birikti: tek client'ın output buffer'ı 101 MB, used_memory 61 MB'tan 162 MB'a çıktı. Normal client'lar için client-output-buffer-limit varsayılan olarak normal 0 0 0, yani sınırsız. Production'da MONITOR'ü birkaç saniyeden uzun açık bırakmak, Redis'in bellek limitine uygulamanın verisiyle değil bir debug oturumuyla dayanması demek. Hangi komutların yavaş olduğunu merak ediyorsan SLOWLOG, hangilerinin sık çağrıldığını merak ediyorsan INFO commandstats daha güvenli.
Grafik bir araç istersen Redis Insight var. Compose dosyasındaki tools profile'ı ile başlatıp tarayıcıda http://localhost:5540'ı açarsın:
docker compose -f lab/compose.yaml --profile tools up -d insight
Bağlantı eklerken host olarak localhost yazma: Insight kendi container'ında çalıştığı için localhost onun kendisi. Denediğimde localhost:6379 424 kodlu RedisConnectionFailedException döndü; compose ağındaki servis adı redis ile (port 6379) hemen bağlandı. Valkey için host valkey.
StackExchange.Redis 3.x ve Tek ConnectionMultiplexer
.NET tarafında client StackExchange.Redis; bu yazı yazılırken güncel kararlı sürüm 3.4.35 (12 Kasım 2026). 3.0 Haziran 2026'da çıktı; sürüm notlarına göre public API'yi bilerek değiştirmeden ağ katmanını ve parser'ı yeniden yazdı ve RESP3'ü (Redis Serialization Protocol 3; Redis 6 ile gelen, pub/sub mesajlarının ve zengin tiplerin aynı bağlantıda taşınabildiği protokol sürümü) varsayılan yaptı. Paketin dokümanı ana kuralı açıkça koyuyor: ConnectionMultiplexer thread-safe'tir, paylaşılmak için tasarlanmıştır ve işlem başına oluşturulmamalıdır. Bookstore.Api'de uygulama başına tek bir tane açıp DI (Dependency Injection; bağımlılıkların class'a dışarıdan verilmesi) container'ına singleton olarak veriyoruz:
using Bookstore.Api;
using StackExchange.Redis;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// Uygulamanın tamamı için tek multiplexer; her istek bunu paylaşır
string redisConnection = builder.Configuration.GetConnectionString("Redis")
?? throw new InvalidOperationException("ConnectionStrings:Redis is missing.");
IConnectionMultiplexer redis = await ConnectionMultiplexer.ConnectAsync(redisConnection);
builder.Services.AddSingleton(redis);
WebApplication app = builder.Build();
app.MapBookViews();
app.Run();
{
"ConnectionStrings": {
"Redis": "localhost:6379,name=bookstore-api,abortConnect=false"
}
}
Connection string'deki iki ayar bilinçli. name=bookstore-api, CLIENT LIST ve SLOWLOG'da bağlantının kime ait olduğunu gösterir; 01'de sorunlu komutu bu sayede bulmuştuk. abortConnect=false olmadan Redis kapalıyken uygulama hiç açılmaz; bu ayarla multiplexer arka planda bağlanmayı dener ve uygulama ayağa kalkar. Endpoint'ler multiplexer'ı DI'dan alıp her çağrıda GetDatabase() çağırıyor. IDatabase ucuz bir object; saklamaya gerek yok:
app.MapPost("/books/{bookId:int}/views", async (int bookId, IConnectionMultiplexer redis) =>
{
long views = await redis.GetDatabase().StringIncrementAsync(RedisKeys.BookViews(bookId));
return Results.Ok(new { bookId, views });
});
dotnet run --project src/Bookstore.Api
curl -X POST http://localhost:5080/books/42/views
curl -X POST http://localhost:5080/books/42/views
curl http://localhost:5080/books/7/views
{"bookId":42,"views":1}
{"bookId":42,"views":2}
{"bookId":7,"views":0}
Önce Tahmin Et'teki "multiplexer başına iki bağlantı" sorusunun cevabı CLIENT LIST'te. Multiplexer bağlanır bağlanmaz ve bir kanala abone olduktan sonra:
after connect:
id=830 name=bookstore-api resp=Resp3 flags=P sub=1 cmd=subscribe
id=831 name=bookstore-api resp=Resp3 flags=N sub=0 cmd=ping
after SubscribeAsync:
id=830 name=bookstore-api resp=Resp3 flags=P sub=2 cmd=subscribe
id=831 name=bookstore-api resp=Resp3 flags=N sub=0 cmd=ping
İkisi de RESP3 konuşuyor. flags=N normal komut bağlantısı, flags=P pub/sub bağlantısı. Uygulama henüz hiçbir kanala abone olmadan pub/sub bağlantısında bir abonelik var: dokümana göre multiplexer, sunucudaki primary/replica değişikliklerini duymak için her sunucuda bir configuration kanalına abone oluyor. RESP3 aslında pub/sub mesajlarını komut bağlantısında taşıyabilir. StackExchange.Redis 3.4.0'dan beri yine de varsayılan olarak ayrı bağlantı kullanıyor, çünkü Redis aboneliği olan bağlantıya pubsub sınıfının çok daha sıkı output buffer limitlerini uyguluyor ve ortak bir bağlantı büyük bir cevap yüzünden kapatılabiliyor. İstersen sharedSubscriptionConnection=true ile tek bağlantıya düşebilirsin; dokümanın şartı, önce sunucudaki pubsub limitlerinin en büyük cevaplarına yettiğini kontrol etmen. Pub/sub'ın kendisi 09'da.
Aynı kod Valkey'e de bağlanıyor; yalnız port farklı. HELLO cevabı ile client'ın gördüğü sürümü karşılaştıralım:
Unspecified/localhost:6379: server=redis, version=8.10.3, protocol=Resp3, ServerType=Standalone, Version=8.10.3
Unspecified/localhost:6380: server=valkey, version=9.1.3, protocol=Resp3, ServerType=Standalone, Version=7.2.4
Valkey HELLO'da kendi adını ve 9.1.3'ü bildiriyor, ama IServer.Version 7.2.4 diyor. 3.4.20 sürüm notlarına göre client, version alanını Redis API sürümü saymadan önce server alanına bakıyor; Valkey için Redis 7.2.4 uyumluluğunu esas alıyor. Kodunda server.Version'a bakıp özellik açıyorsan bunu hesaba kat.
Multiplexing ve Pipelining Gerçekte Nasıl Çalışıyor?
Tek bağlantı darboğaz olmaz mı? StackExchange.Redis'in cevabı multiplexing: farklı çağıranların komutları aynı socket'e art arda yazılır, cevaplar aynı sırayla geri gelir ve her biri doğru Task'ı tamamlar. Bir çağıranın cevabı beklediği süre, diğerlerinin komutlarıyla doldurulur. Etkisini aynı sayacı 10.000 kez dört farklı şekilde artırarak ölçelim:
// 1) her komutu, bir sonrakini göndermeden önce bekle
for (int i = 0; i < count; i++)
{
await db.StringIncrementAsync("bookstore:demo:sequential");
}
// 2) bütün komutları başlat, sonra birlikte bekle: multiplexer bunları pipeline eder
Task<long>[] pending = new Task<long>[count];
for (int i = 0; i < count; i++)
{
pending[i] = db.StringIncrementAsync("bookstore:demo:concurrent");
}
await Task.WhenAll(pending);
// 3) IBatch: client'ta tamponlanır, Execute() çağrılınca socket'e tek seferde yazılır
IBatch batch = db.CreateBatch();
Task<long>[] batched = new Task<long>[count];
for (int i = 0; i < count; i++)
{
batched[i] = batch.StringIncrementAsync("bookstore:demo:batch");
}
batch.Execute();
await Task.WhenAll(batched);
// 4) fire-and-forget: cevap beklenmez, dönen değer her zaman default
for (int i = 0; i < count; i++)
{
await db.StringIncrementAsync("bookstore:demo:faf", flags: CommandFlags.FireAndForget);
}
long faf = (long)await db.StringGetAsync("bookstore:demo:faf"); // normal bir komut; cevaplar sırayla gelir
sequential await : 2,144 ms
Task.WhenAll : 7 ms
IBatch : 16 ms
FireAndForget : 4 ms (value after a normal GET: 10,000)
Sıralı await ile her artış bir round trip bekliyor: 10.000 × ~0,2 ms ≈ 2 saniye. Komutları önce başlatıp sonra birlikte beklediğinde multiplexer onları socket'e peş peşe yazıyor ve Redis'in tek round trip'te birçok komutu işlemesine izin veriyor; bu pipelining. Fark 300 kat. IBatch burada daha hızlı değil; işe yaradığı yer, bir grup komutun başka çağıranların komutlarıyla karışmadan bitişik gitmesini istediğin durum. Atomik değildir, bunun için 08'deki transaction'lar var. FireAndForget cevabı beklemez ve dönen değer anlamsızdır (sayaç yerine her zaman 0). Görüntülenme sayacı gibi kaybı tolere edilebilen yazılar için uygun; sonucunu kullanacağın hiçbir komutta değil. Son GET'in 10.000 döndürmesi de modelin bir sonucu: aynı bağlantıdaki komutlar gönderildiği sırayla çalışır.
Bu model bir şeyi de imkânsız kılıyor: BLPOP gibi blocking komutlar. Bir çağıran paylaşılan bağlantıyı saniyelerce bekletirse herkes bekler; paketin dokümanı bu yüzden blocking pop'ları desteklemediğini ve desteklemeyeceğini yazıyor. List ile kuyruk yaparken buna 05'te döneceğiz.
Async API ve Sync-over-Async
StackExchange.Redis'in her komutunun sync ve async sürümü var. ASP.NET Core'da kural basit: async olanı await et. Neden önemli olduğunu ölçelim. 500 istek aynı anda geliyor ve her biri Redis çağrısını .Result ile bloklayarak bekliyor:
Task[] requests = [.. Enumerable.Range(0, 500).Select(_ => Task.Run(() =>
{
try
{
long views = db.StringIncrementAsync("bookstore:book:42:views").Result;
}
catch (AggregateException ex) when (ex.InnerException is RedisTimeoutException timeout)
{
Interlocked.Increment(ref failed);
firstError ??= timeout.Message;
}
}))];
await Task.WhenAll(requests);
Derlerken paketin kendi analyzer'ı uyarıyor:
SyncOverAsyncDemo.cs(21,30): warning SER307: 'StringIncrementAsync' is being waited on synchronously, which ties up a thread until redis replies - and the reply needs a thread of its own to be processed; await it instead (https://seredis.dev/SyncOverAsync)
Uyarıyı geçip çalıştırdım (asyncTimeout=2000):
500 blocking requests: 109,739 ms, timeouts: 492
Timeout awaiting response (outbound=0KiB, inbound=0KiB, 109705ms elapsed, timeout is 2000ms), command=INCR, next: INCR bookstore:book:42:views, inst: 0, qu: 0, qs: 0, aw: False, bw: SpinningDown, rs: ReadAsync, ws: Idle, in: 6, in-pipe: 0, last-in: 361, cur-in: 361, sync-ops: 0, async-ops: 500, serverEndpoint: localhost:6379, conn-sec: 109.74, aoc: 1, mc: 1/1/0, clientName: bookstore-api, IOCP: (Busy=0,Free=1000,Min=1,Max=1000), WORKER: (Busy=501,Free=32266,Min=10,Max=32767), POOL: (Threads=501,QueuedItems=4,CompletedItems=24,Timers=1), v: 3.4.35.20751 (see https://seredis.dev/Timeouts for some common client-side issues that can cause timeouts)
500 awaited requests: 3 ms
500 INCR 110 saniye sürdü ve 492'si timeout'a düştü. Aynı 500 istek await ile 3 ms. Mesajdaki ipucu WORKER: (Busy=501, … Min=10): thread pool'daki 501 thread'in hepsi .Result'ta bekliyor ve cevabı işleyecek bir thread kalmamış. .NET thread pool'u minimum sayının (burada işlemci sayısı, 10) üstüne çıkınca yeni thread'i yavaş ekliyor; her yeni thread de gelip bloklandığı için tablo düzelmiyor. Redis'in kendisi bu sırada boşta. Paketin dokümanı iki şeyi özellikle vurguluyor: sync API'ye geçmek çözüm değil, çünkü o da çağıran thread'i cevap gelene kadar tutuyor; MinThreads'i artırmak da duvarı yalnız ileri taşıyor. Tek gerçek çözüm çağrı zincirini baştan sona async yapmak. Timeout mesajını okumayı 14'te ayrıntılı işleyeceğiz.
Key Tasarımı ve KEYS Yerine SCAN
Bookstore'da key'ler tek bir kalıba uyuyor: bookstore:<entity>:<id>[:<detay>]. Örneğin bookstore:book:42:views, bookstore:cart:7, bookstore:customer:7:recent. İki nokta üst üste Redis için özel bir karakter değil, yalnız bir gelenek; ama Redis Insight key'leri buna göre ağaç olarak gösteriyor ve ACL'de ~bookstore:* gibi key pattern izinleri yazmayı kolaylaştırıyor. Key adlarını kod içinde string birleştirerek dağıtmak yerine tek bir yerde topluyoruz:
using StackExchange.Redis;
namespace Bookstore.Api;
// Bookstore key isimlendirmesi: bookstore:<entity>:<id>[:<detail>]
public static class RedisKeys
{
private const string Prefix = "bookstore";
public static RedisKey BookViews(int bookId) => $"{Prefix}:book:{bookId}:views";
}
Okunabilir key'lerin bir maliyeti var, ama çoğu zaman sanıldığından küçük. Bir milyon sayacı iki farklı adlandırmayla yazıp used_memory farkına baktım:
bookstore:book:{id}:views 1,000,000 keys: 59.2 MB, MEMORY USAGE of one key: 42 bytes
b:{id}:v 1,000,000 keys: 43.9 MB, MEMORY USAGE of one key: 25 bytes
Milyon key başına 15 MB. Key'i şifreye çevirmek yerine value'ları küçük tutmak ve gereksiz key üretmemek çoğu zaman daha büyük kazanç getirir. Versiyonlu key'ler ve cache key'lerinin tasarımı Caching serisinin "IDistributedCache ve Redis'i Cache Olarak İşletmek" yazısında anlatıldı.
Key'leri listelemen gerektiğinde KEYS değil SCAN kullan. StackExchange.Redis 3.x bu konuda seni zaten durduruyor: 01'de KEYS'i Execute ile göndermeye çalıştığımda client This operation is not available unless admin mode is enabled: KEYS diye reddetti ve allowAdmin=true istedi. Doğru yol IServer.KeysAsync; arka planda SCAN'i sayfa sayfa çağırıyor. 01'deki deneyi KEYS yerine bununla tekrarladım:
// IServer.KeysAsync SCAN kullanır: tek uzun çağrı yerine çok sayıda küçük çağrı
IServer server = instanceB.GetServers()[0];
long count = 0;
await foreach (RedisKey _ in server.KeysAsync(pattern: "bookstore:book:*", pageSize: 1000))
{
count++;
}
instance B: SCAN found 1,000,000 keys in 1,031 ms
instance A: 90 PINGs, median 0.36 ms, p99 6.40 ms, max 6.40 ms
cmdstat_scan:calls=2000,usec=616106,usec_per_call=308.05
Toplam süre KEYS'ten uzun (1 saniye), ama 1.000 ayrı çağrıya bölünmüş; her SCAN sunucuda ortalama 0,3 ms çalışıyor ve araya diğer client'ların komutları giriyor. instance-a'nın en uzun PING'i 102 ms'den 6 ms'ye indi. Yine de SCAN bütün keyspace'i dolaşır; production'da sık çalıştırılacak bir sorgu değil. "Kaç kitabın sayacı var?" gibi bir soru sık soruluyorsa cevabı ayrı bir sayaçta ya da set'te tut. SCAN'in bir garantisi de eksik: tarama sırasında eklenen ya da silinen key'ler sonuçta görünebilir de görünmeyebilir de.
Üretimde Dikkat
Multiplexer'ı uygulama başına bir tane yap ve DI'a singleton olarak ver; aynı uygulamada AddStackExchangeRedisCache gibi kendi bağlantısını açan bir kütüphane kullanıyorsan, Caching serisinde gösterdiğimiz gibi multiplexer'ı ona da paylaştır. Her client'a name= ver: CLIENT LIST'te 400 tane isimsiz bağlantı görmek teşhisi zorlaştırır. connected_clients'ı izle ve beklenen sayıyla karşılaştır; instance başına iki bağlantı normaldir, istek sayısıyla büyüyen bir sayı sızıntıdır. MONITOR'ü production'da kullanma, kullanman gerekirse birkaç saniyeyle sınırla. Redis portunu container'da da olsa yalnız gereken ağa aç; resmî image'larda protected-mode'un kapalı olduğunu unutma. Valkey ile aynı kodu çalıştırabiliyor olman, aynı davranışı garanti etmez; kullandığın komutları iki tarafta da test et.
Sık Düşülen Tuzaklar
İstek başına ConnectionMultiplexer: 200 istek için 400 TCP bağlantısı ve 10 kat yavaş cevap. Dispose unutulursa bağlantılar birikir.
Async kodda .Result, .Wait() ya da sync API: yük altında thread pool tükenir, Redis boşta beklerken uygulama timeout üretir. SER307 uyarısını bastırmak yerine çağrıyı await et.
Production'da KEYS *: tek komut bütün client'ları bekletir. IServer.KeysAsync (SCAN) kullan, o da seyrek.
FireAndForget'in dönüş değerini kullanmak: değer her zaman default'tur, sunucunun cevabı değildir.
Insight'ta ya da başka bir container'da localhost'a bağlanmaya çalışmak: container içinden localhost o container'ın kendisidir; compose ağında servis adını kullan.
Küçük Bir Deneme
★☆☆ Lab'ı compose ile kur, Bookstore.Api'yi çalıştır ve docker compose -f lab/compose.yaml exec redis redis-cli CLIENT LIST ile bookstore-api adlı bağlantıları say. API'ye 100 istek gönderdikten sonra sayı değişiyor mu?
★★☆ pipeline deneyini BOOKSTORE_REDIS=localhost:6380 ile Valkey'e karşı çalıştır. Sonra Task.WhenAll bölümünde komut sayısını 100.000'e çıkar: süre doğrusal mı büyüyor?
★★★ Bookstore.Api'nin connection string'ine sharedSubscriptionConnection=true ekle ve CLIENT LIST'e tekrar bak. Kaç bağlantı var, flags alanında ne değişti? CONFIG GET client-output-buffer-limit ile pubsub sınırlarını oku ve bu ayarın büyük bir HGETALL cevabıyla neden riskli olabileceğini açıkla.
Bir sonraki yazıda Redis'in en temel tipine, string'e dönüyoruz: SET'in seçenekleri, sayaçlar, Redis 8.4'ün compare-and-set komutları ve expiration'ın içeride gerçekte nasıl çalıştığı.