Redis Pub/Sub ve Keyspace Notification'ları: Sharded Pub/Sub, At-Most-Once ve Cache Invalidation Sinyali
Redis pub/sub mesajı yalnız o an bağlı olan dinleyicilere iletir ve hiçbir yerde saklamaz; bağlantı koptuğu sırada yayınlanan mesaj sonradan da gelmez. Gerçek çıktılarla at-most-once teslimat, handler ile ChannelMessageQueue'nun mesaj sırası, paylaşılan bağlantıda 40 MB'lık GET'in kopması, iki API instance'ında fiyat değişince L1 invalidation, kuyruk için iş var sinyali, sharded pub/sub, gecikmeli expired olayları, Redis 8.8'in hash field notification'ları ve yavaş subscriber'ın 32 MB sınırı.
Sürüm notu: Bu yazıdaki örnekler Redis 8.10.3 (redis:8), Valkey 9.1.3, .NET 10 ve StackExchange.Redis 3.4.35 ile çalıştırılmıştır.
Önce Tahmin Et
Bookstore'da (sitedeki serilerin ortak örnek uygulaması, bir online kitapçı) fiyat yönetim ekranı bir kitabın fiyatı değişince bookstore:events:price-changed kanalına kitabın ISBN'ini (13 haneli uluslararası kitap numarası) yayınlıyor; API instance'ları bu kanalı dinleyip kendi bellek cache'lerinden o kitabı siliyor. Aşağıdaki akışta dört fiyat değişikliği yayınlanıyor: biri dinleyici abone olmadan önce, biri abone olduktan sonra, biri sunucu bütün pub/sub bağlantılarını kopardığı anda, sonuncusu üç saniye sonra:
Log($"PUBLISH 9780441013593 -> receivers: {await pub.PublishAsync(PriceChanged, "9780441013593")}");
await listener.GetSubscriber().SubscribeAsync(PriceChanged, (_, isbn) => Log($"listener: received {isbn}"));
Log($"PUBLISH 9780451524935 -> receivers: {await pub.PublishAsync(PriceChanged, "9780451524935")}");
await Task.Delay(200);
// Sunucu bütün pub/sub bağlantılarını koparıyor (bir ağ sorunu ya da failover gibi)
long killed = await publisher.GetServers()[0].ClientKillAsync(null, ClientType.PubSub, null, skipMe: true);
Log($"CLIENT KILL TYPE pubsub -> {killed} connection(s)");
Log($"PUBLISH 9780141439518 -> receivers: {await pub.PublishAsync(PriceChanged, "9780141439518")}");
await Task.Delay(3000);
Log($"PUBLISH 9780547928227 -> receivers: {await pub.PublishAsync(PriceChanged, "9780547928227")}");
Dinleyici dört mesajdan hangilerini alır? Kaçırdıklarını bağlantı geri gelince sonradan alır mı?
dotnet run --project src/Bookstore.RedisLab -- pubsub-delivery
0.00s PUBLISH 9780441013593 -> receivers: 0
0.00s PUBLISH 9780451524935 -> receivers: 1
0.01s listener: received 9780451524935
0.21s CLIENT KILL TYPE pubsub -> 2 connection(s)
0.21s PUBLISH 9780141439518 -> receivers: 0
3.21s listener: received 9780547928227
3.21s PUBLISH 9780547928227 -> receivers: 1
Dinleyici yalnız ikinci ve dördüncü mesajı aldı. İlki o an kimse dinlemediği için, üçüncüsü bağlantı koptuğu ve StackExchange.Redis henüz yeniden abone olmadığı için kayboldu; PUBLISH'in dönüş değeri olan alıcı sayısı ikisinde de 0. Redis kaybolan mesajı hiçbir yerde saklamıyor, sonradan da teslim etmiyor. Pub/sub'ın teslim garantisi at-most-once: bir mesaj en fazla bir kez teslim edilir, hiç edilmeyebilir. Bu yazıda pub/sub'ı bu sınırla birlikte nerede kullanacağımızı anlatıyoruz: StackExchange.Redis'te abonelik ve mesaj sırası, ayrı pub/sub bağlantısı, birden çok API instance'ına cache invalidation sinyali, 05'teki kuyruk için "iş var" sinyali, sharded pub/sub, keyspace notification'ları ve Redis 8.8'in hash field seviyesindeki notification'ları.
PUBLISH, SUBSCRIBE ve At-Most-Once
Pub/sub'da key yok, kanal var. SUBSCRIBE kanal bir bağlantıyı o kanalın dinleyicisi yapar; PSUBSCRIBE bookstore:events:* glob pattern'le birden çok kanalı dinler; PUBLISH kanal mesaj mesajı o anda bağlı olan bütün dinleyicilere iletir ve kaç kişiye ilettiğini döndürür. Redis mesajı bir yere yazmaz; persistence ayarı ne olursa olsun pub/sub mesajları RDB'ye (verinin belirli anlarda diske yazılan snapshot'ı) ya da AOF'a (yazma komutlarının sırayla eklendiği log dosyası) girmez. Bağlantı koptuğunda StackExchange.Redis yeniden bağlanır ve abonelikleri kendisi yeniler; Önce Tahmin Et'te dördüncü mesajın gelmesi bundan. Ama aradaki mesajlar gitmiştir.
Bu yüzden pub/sub, kaçırıldığında sistemin kendini toparlayabildiği sinyaller için uygun: "şu kitabın fiyatı değişti, cache'ini sil", "kuyrukta iş var, bak". Kaçırılması bir müşterinin e-postasının hiç gitmemesi anlamına gelen işler için değil; onlar için 10'daki Streams var.
StackExchange.Redis'te Abonelik: Handler mı, ChannelMessageQueue mu?
ISubscriber.SubscribeAsync'in iki biçimi var: bir callback alan ve bir ChannelMessageQueue döndüren. Paketin dokümanına göre callback'li biçim mesajları eşzamanlı (concurrent) işleyebilir ve sıra garantisi vermez; queue'lu biçim mesajları geldiği sırayla, birer birer işler. On mesajı iki kanala yayınlayıp her birinde 1–20 ms arası değişen bir iş yaptım:
await sub.SubscribeAsync(handlerChannel, (_, m) =>
{
Thread.Sleep(random.Next(1, 20)); // farklı uzunlukta bir iş
handlerSeen.Enqueue((int)m);
});
ChannelMessageQueue queue = await sub.SubscribeAsync(queueChannel);
queue.OnMessage(async message =>
{
await Task.Delay(random.Next(1, 20));
queueSeen.Enqueue((int)message.Message);
});
dotnet run --project src/Bookstore.RedisLab -- pubsub-order
handler : 3 5 1 6 8 4 9 2 10 7
ChannelMessageQueue: 1 2 3 4 5 6 7 8 9 10
Callback'te sıra tamamen karıştı. Cache invalidation'da sıra önemli değil (hangi kitabın önce silindiği fark etmez) ve eşzamanlı işleme daha hızlı; ama "fiyat 300 TL oldu", ardından "fiyat 280 TL oldu" gibi durum taşıyan mesajlarda sıra karışırsa eski değer kazanır. O durumda queue'yu kullan ya da mesajın içine bir sürüm numarası koy.
02'de gördüğümüz gibi her multiplexer iki bağlantı açıyor: komutlar için bir interactive bağlantı ve pub/sub için ayrı bir bağlantı. RESP3 (Redis Serialization Protocol 3; pub/sub mesajlarını komutlarla aynı bağlantıda taşıyabilen protokol sürümü) aslında tek bağlantıyla yetinebilir ve StackExchange.Redis'te bunun için sharedSubscriptionConnection=true ayarı var; 3.4.0 ile varsayılan ayrı bağlantı oldu. Paketin configuration dokümanı nedenini yazıyor: Redis, aboneliği olan her bağlantıya çok daha sıkı pubsub output buffer sınırlarını uyguluyor. Bunu denedim; 40 MB'lık bir raporu iki ayarla okudum:
dotnet run --project src/Bookstore.RedisLab -- shared-subscription
shared=False GET 40 MB: 40 MB received, connection failures=0
shared=True GET 40 MB: RedisConnectionException: SocketClosed on localhost:6379/Interactive, Idle/RanToCompletion, last: GET, origin: ReadAllAsync, outstanding, connection failures=1
Paylaşılan bağlantıda multiplexer'ın kendi configuration kanalı aboneliği bile bağlantıyı Redis'in gözünde bir pub/sub client'ı yapıyor (02'deki flags=P). 40 MB'lık cevap 32 MB'lık pubsub sınırını aştı ve Redis bağlantıyı kapattı; o bağlantıdaki bütün bekleyen komutlar da onunla gitti. Varsayılanı değiştirmek için iyi bir nedenin yoksa değiştirme.
Bookstore: Fiyat Değişince L1 Invalidation
Bookstore.Api'nin her instance'ı kitap fiyatlarını 10 dakikalığına kendi belleğinde (L1, IMemoryCache) tutuyor; asıl değer Redis'te bir hash'te (bookstore:prices, kuruş cinsinden). Bir instance'ta fiyat değiştiğinde diğerlerinin L1'i eski fiyatı göstermeye devam eder; Caching serisinin "HybridCache Derinlemesine: L1/L2, Tag'ler, Logical Invalidation ve Çok Sunuculu Ortamda L1 Tutarsızlığı" yazısında anlatılan sorun bu. Çözüm tarafı ve diğer seçenekler o yazıda ve "Cache Invalidation Stratejileri" yazısında; burada Redis tarafını kuruyoruz. Fiyatı yazan endpoint değişikliği yayınlıyor:
// Yeni fiyatı yaz, sonra bütün instance'lara L1 kopyalarını silmelerini söyle
app.MapPut("/books/{isbn}/price", async (string isbn, PriceRequest request, IConnectionMultiplexer redis) =>
{
IDatabase db = redis.GetDatabase();
await db.HashSetAsync(RedisKeys.Prices, isbn, request.PriceInKurus);
long receivers = await db.PublishAsync(RedisKeys.PriceChanged, isbn); // PUBLISH
return Results.Ok(new { isbn, request.PriceInKurus, notifiedInstances = receivers });
});
Her instance'ta bir BackgroundService kanalı dinliyor ve gelen ISBN'i kendi L1'inden siliyor. Bağlantı geri geldiğinde ise L1'in tamamını temizliyor, çünkü bağlantı yokken gelen mesajlar kayboldu ve hangi kitapların değiştiğini bilmenin yolu yok:
// Fiyat değişikliklerini dinler ve kitabı bu instance'ın L1 cache'inden siler
public sealed class PriceInvalidationListener(IConnectionMultiplexer redis, IMemoryCache local,
ILogger<PriceInvalidationListener> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Bağlantı yokken gelen mesajlar kayboldu: L1'i boşaltıp baştan başla
redis.ConnectionRestored += (_, e) =>
{
if (e.ConnectionType == ConnectionType.Subscription && local is MemoryCache memory)
{
memory.Clear();
logger.LogWarning("Subscription connection restored, L1 price cache cleared");
}
};
ChannelMessageQueue queue = await redis.GetSubscriber().SubscribeAsync(RedisKeys.PriceChanged);
try
{
await foreach (ChannelMessage message in queue.WithCancellation(stoppingToken))
{
local.Remove(BookPriceEndpoints.CacheKey(message.Message!));
logger.LogInformation("Price of {Isbn} changed, L1 entry removed", (string?)message.Message);
}
}
catch (OperationCanceledException)
{
}
finally
{
await queue.UnsubscribeAsync();
}
}
}
İki instance'ı 5080 ve 5081 portlarında başlattım (dotnet run --project src/Bookstore.Api -- --urls http://localhost:5081 --InstanceName api-2):
curl http://localhost:5080/books/9780441013593/price
{"isbn":"9780441013593","priceInKurus":29990,"source":"redis","instance":"api-1"}
curl http://localhost:5081/books/9780441013593/price
{"isbn":"9780441013593","priceInKurus":29990,"source":"redis","instance":"api-2"}
curl http://localhost:5081/books/9780441013593/price
{"isbn":"9780441013593","priceInKurus":29990,"source":"l1","instance":"api-2"}
curl -X PUT http://localhost:5080/books/9780441013593/price -H "Content-Type: application/json" -d '{"priceInKurus":27990}'
{"isbn":"9780441013593","priceInKurus":27990,"notifiedInstances":2}
curl http://localhost:5081/books/9780441013593/price
{"isbn":"9780441013593","priceInKurus":27990,"source":"redis","instance":"api-2"}
info: Bookstore.Api.PriceInvalidationListener[0]
Price of 9780441013593 changed, L1 entry removed
warn: Bookstore.Api.PriceInvalidationListener[0]
Subscription connection restored, L1 price cache cleared
Fiyat 5080'de değişti, 5081 bir sonraki istekte yeni fiyatı Redis'ten okudu. Log'daki ikinci satır, deneme sonunda CLIENT KILL TYPE pubsub ile aboneliği kopardığımda yazıldı. Sinyal kaybolabileceği için L1 ömrünü de kısa tutmak gerekiyor: 10 dakika, kaçan bir sinyalin en kötü durumda ne kadar eski fiyat gösterebileceğinin üst sınırı. SignalR'ın Redis backplane'i de aynı mekanizmayla çalışıyor: her sunucu ortak kanallara abone olup mesajları diğer sunuculardaki client'lara iletiyor. Ayrıntısı Real-time Uygulamalar serisinin "SignalR'ı Scale-Out Etmek ve Production'da İşletmek" yazısında.
Kuyrukta İş Var Sinyali
05'te StackExchange.Redis'te BLPOP'un paylaşılan bağlantıyı dondurduğunu görmüş ve dokümanın önerisini not etmiştik: veri list'te kalsın, pub/sub yalnız "iş var" desin. E-posta worker'ı artık uyumak için kanalı bekliyor, ama en fazla 2 saniye; sinyal kaybolursa yoklama (polling) devreye giriyor:
while (!stop.IsCancellationRequested)
{
RedisValue job;
while (!(job = await workerDb.ListMoveAsync(queue, processing, ListSide.Right, ListSide.Left)).IsNull)
{
Console.WriteLine($" worker: {job} picked up after {sw.ElapsedMilliseconds - enqueuedAt[job!],5} ms");
await workerDb.ListRemoveAsync(processing, job, count: 1);
}
// Bir sinyal gelene kadar uyu, ama yoklama aralığından uzun değil
using CancellationTokenSource poll = CancellationTokenSource.CreateLinkedTokenSource(stop.Token);
poll.CancelAfter(TimeSpan.FromSeconds(2));
try
{
await wakeUps.ReadAsync(poll.Token);
}
catch (OperationCanceledException)
{
}
}
dotnet run --project src/Bookstore.RedisLab -- queue-signal
api: LPUSH order-1001, PUBLISH signal -> 1
worker: order-1001 picked up after 12 ms
api: LPUSH order-1002, PUBLISH signal -> 1
worker: order-1002 picked up after 2 ms
api: LPUSH order-1003, signal lost
worker: order-1003 picked up after 1310 ms
Sinyal gelen işler milisaniyeler içinde alındı; sinyali kaybolan iş, bir sonraki yoklamada 1,3 saniye sonra. Yoklama aralığı, kaybolan bir sinyalin en fazla ne kadar gecikmeye yol açacağını belirliyor. Paylaşılan multiplexer'ın hiçbir komutu beklemiyor ve boş kuyruğa saniyede yüzlerce LMOVE gitmiyor.
Sharded Pub/Sub
Redis Cluster'da klasik PUBLISH her mesajı cluster'daki bütün node'lara yayar; node sayısı arttıkça bu trafik de artar. Redis 7.0 ile gelen sharded pub/sub (SSUBSCRIBE, SPUBLISH) kanal adını key gibi bir slot'a bağlıyor ve mesajı yalnız o slot'un shard'ında tutuyor. Tek sunuculu lab'da da çalışıyor:
127.0.0.1:6379> SPUBLISH bookstore:events:price-changed:{9780441013593} 27990
(integer) 1
127.0.0.1:6379> PUBSUB SHARDNUMSUB bookstore:events:price-changed:{9780441013593}
1) "bookstore:events:price-changed:{9780441013593}"
2) (integer) 1
127.0.0.1:6379> PUBLISH bookstore:events:price-changed:{9780441013593} 27990
(integer) 0
Arka planda SSUBSCRIBE ile dinleyen bir redis-cli mesajı aldı; aynı adlı kanala klasik PUBLISH ise 0 alıcıya ulaştı, çünkü iki mekanizmanın kanalları ayrı. StackExchange.Redis'te RedisChannel.Sharded("…") ile oluşturulan bir kanal SSUBSCRIBE/SPUBLISH kullanıyor. Kanal adında da hash tag kullanabilirsin; slot ve shard ayrıntısı 13'te. Valkey 9.1.3'te sharded pub/sub komutları var.
Keyspace Notification'ları
Redis key'lerde olan değişiklikleri de pub/sub kanallarına yayınlayabiliyor: __keyspace@0__:key kanalı o key'deki olayları, __keyevent@0__:olay kanalı o olayın yaşandığı key'leri taşıyor. Varsayılan olarak kapalı; notify-keyspace-events ayarıyla açılıyor. Ex, yalnız key-event kanallarında (E) yalnız expire olaylarını (x) açar. Cazip bir fikir: "doğrulama kodu süresi dolunca haber ver", "stok rezervasyonu süresi dolunca stoğu geri koy". Süresi tam aynı anda dolan 20.000 key ile expired olaylarının ne zaman geldiğini ölçtüm:
dotnet run --project src/Bookstore.RedisLab -- expired-events
expired events: 20,000; after the TTL reached zero: first 99 ms, median 129 ms, last 299 ms
Olaylar TTL (key'in kalan yaşam süresi) sıfırlandığı anda değil, Redis key'i gerçekten sildiğinde geliyor. 03'te anlattığımız gibi bu ya key'e erişildiğinde ya da arka plandaki aktif expire döngüsünde oluyor; burada ilk olay 99 ms, sonuncusu 299 ms gecikti. Dokümana göre çok sayıda TTL'li key varken ve key'lere erişilmiyorsa bu gecikme ciddi boyutlara çıkabilir. Üstüne pub/sub'ın at-most-once teslimatı ve Cluster'da her node'un yalnız kendi key'lerinin olaylarını yayınlaması (bütün olaylar için her node'a ayrı abone olmak gerekiyor) ekleniyor. Bu yüzden expired olayını iş mantığının tek tetikleyicisi yapma: süresi dolan rezervasyonları stoğa geri koymak gerekiyorsa 08'in Küçük Bir Deneme'sindeki gibi bir sorted set'i periyodik olarak tarayan bir job yaz; olay en fazla o job'ı erkenden uyandıran bir sinyal olsun.
Hash Field Notification'ları ve Client-Side Caching
Klasik keyspace notification'ları bir hash'te HSET olduğunu söyler ama hangi field'ın değiştiğini söylemez. Redis 8.8 subkey notification'ları getirdi; olay mesajı artık field adlarını da taşıyor. 04'teki sepet fiyat kilitleriyle (HSETEX ile field bazında TTL) denedim:
127.0.0.1:6379> CONFIG SET notify-keyspace-events STh
OK
127.0.0.1:6379> HSETEX bookstore:cart:7:price-locks PX 1500 FIELDS 2 9780441013593 29990 9780141439518 18950
(integer) 1
127.0.0.1:6379> HSET bookstore:cart:7:price-locks 9780451524935 22500
(integer) 1
127.0.0.1:6379> HDEL bookstore:cart:7:price-locks 9780451524935
(integer) 1
$ docker compose -f lab/compose.yaml exec redis redis-cli --csv psubscribe "__subkey*"
"psubscribe","__subkey*",1
"pmessage","__subkey*","__subkeyspace@0__:bookstore:cart:7:price-locks","hset|13:9780441013593,13:9780141439518"
"pmessage","__subkey*","__subkeyevent@0__:hset","28:bookstore:cart:7:price-locks|13:9780441013593,13:9780141439518"
"pmessage","__subkey*","__subkeyspace@0__:bookstore:cart:7:price-locks","hexpire|13:9780441013593,13:9780141439518"
"pmessage","__subkey*","__subkeyevent@0__:hexpire","28:bookstore:cart:7:price-locks|13:9780441013593,13:9780141439518"
"pmessage","__subkey*","__subkeyspace@0__:bookstore:cart:7:price-locks","hset|13:9780451524935"
"pmessage","__subkey*","__subkeyevent@0__:hset","28:bookstore:cart:7:price-locks|13:9780451524935"
"pmessage","__subkey*","__subkeyspace@0__:bookstore:cart:7:price-locks","hdel|13:9780451524935"
"pmessage","__subkey*","__subkeyevent@0__:hdel","28:bookstore:cart:7:price-locks|13:9780451524935"
"pmessage","__subkey*","__subkeyspace@0__:bookstore:cart:7:price-locks","hexpired|13:9780141439518,13:9780441013593"
"pmessage","__subkey*","__subkeyevent@0__:hexpired","28:bookstore:cart:7:price-locks|13:9780141439518,13:9780441013593"
S key bazlı (__subkeyspace), T olay bazlı (__subkeyevent) kanalları açıyor; field adları uzunluk önekiyle (13:9780441013593) geliyor ki içinde virgül olan adlar da güvenle ayrıştırılabilsin. İki fiyat kilidinin süresi dolunca tek bir hexpired mesajı ikisini birlikte bildirdi. Burada dokümanla ölçüm arasında bir fark var: subkey notification sayfasındaki örnek yalnız ST ayarlayıp HSET ile mesaj alıyor. Redis 8.10.3'te ST ile hiçbir mesaj gelmedi; aynı sayfanın flag listesindeki h'yi (hash komutları) de ekleyince (STh) geldi. Valkey 9.1.3 STh'yi geçersiz karakter hatasıyla reddetti; subkey notification'ları Valkey'de yok. StackExchange.Redis 3.4'te RedisChannel.SubKeyEvent gibi kanal yardımcıları ve mesajdaki field'ları ayrıştıran ChannelMessage.TryParseKeyNotification var.
Notification'larla yakından ilişkili bir özellik de client-side caching: CLIENT TRACKING ON diyen bir bağlantı için Redis, o bağlantının okuduğu key'leri hatırlıyor ve key değişince bir invalidation mesajı gönderiyor (RESP3'te aynı bağlantıda, RESP2'de (protokolün pub/sub için ayrı bağlantı gerektiren eski sürümü) başka bir bağlantıya yönlendirilerek). Bu, yukarıda elle kurduğumuz L1 invalidation'ın sunucu tarafından yapılan hâli. StackExchange.Redis 3.4.35'in public API'sinde bunu kullanan hazır bir L1 cache katmanı görmedim; .NET tarafında bu iş HybridCache ve Caching serisinin konusu.
Üretimde Dikkat
Pub/sub'ın en sık karşılaşılan production sorunu yavaş subscriber. Redis bir mesajı dinleyicinin TCP bağlantısına yazamıyorsa onun output buffer'ında biriktiriyor. Hiç okumayan bir subscriber açıp 16 KB'lık mesajlar yayınladım:
dotnet run --project src/Bookstore.RedisLab -- slow-subscriber
client-output-buffer-limit: normal 0 0 0 slave 268435456 67108864 60 pubsub 33554432 8388608 60
published 500 x 16 KB receivers=1 slow-subscriber: omem=5043984
published 1,000 x 16 KB receivers=1 slow-subscriber: omem=13286592
published 1,500 x 16 KB receivers=1 slow-subscriber: omem=21529200
published 2,000 x 16 KB receivers=1 slow-subscriber: omem=29771808
published 2,231 x 16 KB receivers=0 slow-subscriber: gone
docker compose -f lab/compose.yaml logs redis
... name=slow-subscriber ... flags=P ... omem=33565048 ... cmd=subscribe ... scheduled to be closed ASAP for overcoming of output buffer limits.
pubsub 33554432 8388608 60: buffer 32 MB'ı geçerse bağlantı hemen, 60 saniye boyunca 8 MB'ın üstünde kalırsa yine kapatılıyor. 2.231. mesajda buffer 32 MB'ı aştı ve Redis bağlantıyı kopardı; subscriber'ın kaçırdığı mesajlar geri gelmeyecek. Bu sınır sunucunun belleğini koruyor: sınır olmasaydı okumayan tek bir client 02'deki MONITOR gibi belleği şişirirdi. Önlem tarafında: dinleyicide ağır işi mesaj geldiği anda yapma, bir in-memory kuyruğa (ChannelMessageQueue ya da System.Threading.Channels) al ve ayrıca işle; büyük nesneleri mesajla gönderme, yalnız kimlik gönder (ISBN gibi); CLIENT LIST'te flags=P olan bağlantıların omem değerini izle. Keyspace notification'larını yalnız gereken olay sınıfları için aç; KEA her yazma komutu için mesaj üretir ve CPU'ya yük bindirir.
Sık Düşülen Tuzaklar
Pub/sub'ı iş kuyruğu olarak kullanmak: Önce Tahmin Et'te dört mesajın ikisi kayboldu ve kimse fark etmedi. Kaybı kabul edilemeyen işler için Streams (10).
Durum taşıyan mesajları callback'li abonelikle işlemek: on mesaj karışık sırada işlendi; sıra gerekiyorsa ChannelMessageQueue.
Yeniden bağlanmadan sonra hiçbir şey yapmamak: aradaki mesajlar kayboldu; L1'i temizlemek ya da kaynağı yeniden okumak gerekiyor.
sharedSubscriptionConnection=true ile büyük cevaplar okumak: 40 MB'lık GET komut bağlantısını kopardı.
Expired olayını zamanlayıcı sanmak: olay TTL bittiğinde değil, key silindiğinde geliyor ve kaybolabiliyor.
Yavaş subscriber: 32 MB'tan sonra Redis bağlantıyı kapattı.
Redis 8.8'in subkey notification'larını Valkey'de varsaymak ya da yalnız ST ayarlayıp mesaj beklemek.
Küçük Bir Deneme
★☆☆ İki terminalde redis-cli aç; birinde PSUBSCRIBE bookstore:events:*, diğerinde iki farklı kanala PUBLISH yap. PUBSUB NUMPAT ve PUBSUB CHANNELS ne gösteriyor?
★★☆ PriceInvalidationListener'ı callback'li aboneliğe çevir ve mesaja fiyatı da ekle (isbn:fiyat); dinleyici L1'i silmek yerine yeni fiyatla güncellesin. Aynı kitaba art arda iki fiyat yayınladığında ne zaman yanlış fiyat kalıyor? Mesaja bir sürüm numarası ekleyerek düzelt.
★★★ expired-events deneyini 200.000 key ile ve hz ayarını 10'dan 100'e çıkararak tekrarla (yalnız lab'da). İlk ve son olayın gecikmesi nasıl değişiyor? INFO stats'taki expired_keys ile karşılaştır.
Bir sonraki yazıda kaybolmaması gereken mesajlara geçiyoruz: Redis Streams, consumer group'lar, pending entry list, XAUTOCLAIM ile ölen bir worker'ın mesajını devralmak ve Redis 8.6'nın idempotent XADD'i; 05'teki e-posta kuyruğunu bir stream'e taşıyacağız.