Redis String'leri ve Expiration: SET Seçenekleri, Sayaçlar, Compare-and-Set ve TTL Semantiği
Redis'te bir key'in üzerine SET ile yazmak TTL'ini siler; doğrulama kodunu yeniden yazarken EX vermezsen kod hiç silinmez. Gerçek çıktılarla SET seçenekleri ve KEEPTTL, INCR ve EXPIRE NX ile günlük sayaç, GET + SET ile 10.000 artışın 9.617'sinin kaybolması, Redis 8.4'ün SET IFEQ, DELEX ve DIGEST komutlarıyla tek kullanımlık kod ve idempotency key, Valkey farkları ve expiration'ın lazy ve aktif silme ile içeride nasıl çalıştığı.
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çı) yeni üye olan müşteriye e-posta ile 6 haneli bir doğrulama kodu gidiyor. Kod Redis'te 5 dakika yaşıyor. Müşteri e-postayı bulamayıp "kodu tekrar gönder"e basıyor; o butonun arkasındaki kod, yeni kodu aynı key'e yazıyor ama süreyi tekrar vermeyi unutuyor:
127.0.0.1:6379> SET bookstore:verify:7 482913 EX 300
OK
127.0.0.1:6379> TTL bookstore:verify:7
(integer) 300
127.0.0.1:6379> SET bookstore:verify:7 105577
OK
127.0.0.1:6379> TTL bookstore:verify:7
Son TTL ne döner? Eski süreden kalanı mı (yaklaşık 300), yoksa başka bir şey mi?
(integer) -1
-1, "key var ama süresi yok" demek. Yeni kod hiç silinmeyecek. SET, değeri değiştirmekle kalmıyor, key'i yeniden yazıyor ve mevcut TTL'i (Time To Live; key'in silinmesine kalan süre) siliyor. Redis dokümanının kuralı şu: key'in içeriğini silen ya da üzerine yazan komutlar (DEL, SET, GETSET, *STORE komutları) süreyi temizler; değeri yerinde değiştirenler (INCR, LPUSH, HSET) dokunmaz. Doğru çözüm kodu her seferinde EX ile yazmak; mevcut süreyi korumak istediğin durumlar için de KEEPTTL var:
127.0.0.1:6379> SET bookstore:verify:7 482913 EX 300
OK
127.0.0.1:6379> SET bookstore:verify:7 105577 KEEPTTL
OK
127.0.0.1:6379> TTL bookstore:verify:7
(integer) 300
Bu yazıda Redis'in en temel tipi olan string'e bakıyoruz: SET'in seçenekleri, sayaçlar, Redis 8.4 ile gelen compare-and-set komutları ve expiration'ın içeride nasıl çalıştığı. Bookstore'a üç özellik ekliyoruz: günlük görüntülenme sayacı, tek kullanımlık doğrulama kodu ve sipariş için idempotency key.
SET'in Seçenekleri
Redis string'i binary-safe bir byte dizisidir; metin, sayı, serialize edilmiş bir object ya da bir resim olabilir. Tek bir value en fazla 512 MB olabilir (proto-max-bulk-len varsayılanı 536.870.912 byte), ama 01'de büyük value'ların maliyetini gördük. SET yıllar içinde seçenek kazandı ve bugün birkaç ayrı komutun işini tek başına yapıyor:
| Seçenek | Ne yapar | Geldiği sürüm |
|---|---|---|
EX / PX | Saniye / milisaniye cinsinden süre | 2.6.12 |
NX / XX | Yalnız key yoksa / yalnız key varsa yaz | 2.6.12 |
KEEPTTL | Mevcut süreyi koru | 6.0 |
GET | Yazmadan önceki değeri döndür | 6.2 |
EXAT / PXAT | Mutlak Unix zamanında sil | 6.2 |
IFEQ / IFNE | Yalnız değer eşitse / eşit değilse yaz | 8.4 (Valkey'de yalnız IFEQ, 8.1) |
IFDEQ / IFDNE | Değerin digest'i eşitse / değilse yaz | 8.4 (Valkey'de yok) |
127.0.0.1:6379> SET bookstore:lock:report token-a NX PX 30000
OK
127.0.0.1:6379> SET bookstore:lock:report token-b NX PX 30000
(nil)
127.0.0.1:6379> SET bookstore:book:42:price 349.90 XX
(nil)
127.0.0.1:6379> SET bookstore:book:42:title Dune GET
(nil)
127.0.0.1:6379> SET bookstore:book:42:title "Dune (Deluxe)" GET
"Dune"
127.0.0.1:6379> GETEX bookstore:book:42:title EX 60
"Dune (Deluxe)"
127.0.0.1:6379> GETDEL bookstore:verify:7
"105577"
127.0.0.1:6379> EXISTS bookstore:verify:7
(integer) 0
NX ikinci yazıyı reddetti: distributed lock'un komut tarafı bu kadar (neden tek başına yetmediği Distributed Systems Temelleri serisinde). XX key olmadığı için yazmadı. GET seçeneği yazarken eski değeri döndürüyor; eski GETSET'in yerini aldı. Yanındaki iki komut da string'le çalışırken sık işe yarar: GETEX değeri okurken süreyi değiştirir (sliding expiration için), GETDEL okuyup tek adımda siler. Redis 8.4'ün MSETEX'i de birden fazla key'i aynı süreyle tek komutta yazıyor; Valkey'de 9.1'den beri var:
127.0.0.1:6379> MSETEX 2 bookstore:book:42:price 349.90 bookstore:book:1984:price 199.90 EX 600
(integer) 1
127.0.0.1:6379> TTL bookstore:book:1984:price
(integer) 600
Sayaçlar: INCR, INCRBY ve INCRBYFLOAT
Bir string'in içinde 64 bit işaretli tam sayıya çevrilebilen bir değer varsa Redis onu sayaç gibi kullanmana izin verir. 01'de gördüğümüz gibi bu komutlar atomiktir; aynı anda gelen artışlar birbirini ezmez:
127.0.0.1:6379> SET bookstore:book:42:views 1250
OK
127.0.0.1:6379> INCR bookstore:book:42:views
(integer) 1251
127.0.0.1:6379> INCRBY bookstore:book:42:views 10
(integer) 1261
127.0.0.1:6379> DECR bookstore:book:42:views
(integer) 1260
127.0.0.1:6379> SET bookstore:book:42:rating 4.1
OK
127.0.0.1:6379> INCRBYFLOAT bookstore:book:42:rating 0.1
"4.2"
127.0.0.1:6379> INCR bookstore:book:42:title
(error) ERR value is not an integer or out of range
127.0.0.1:6379> SET bookstore:stock:9780441013593 9223372036854775807
OK
127.0.0.1:6379> INCR bookstore:stock:9780441013593
(error) ERR increment or decrement would overflow
Sayı olmayan bir değeri artırmak ve 64 bit sınırını aşmak hata verir; Redis sessizce taşmaz. INCRBYFLOAT kesirli artış yapar, ama sonucu string olarak tutar ve para gibi hassas hesaplar için uygun değildir; Bookstore'da fiyatlar kuruş cinsinden tam sayı ya da veritabanında decimal olarak durur.
Bookstore'un kitap görüntülenme endpoint'i 02'de yalnız toplam sayacı artırıyordu. Şimdi bir de günlük sayaç ekliyoruz: bookstore:book:42:views:20261114. Günlük key'ler sonsuza kadar birikmesin diye 8 günlük bir süre alıyor, ama yalnız ilk artışta. Bunun için EXPIRE … NX (Redis 7.0+) var; StackExchange.Redis'te karşılığı ExpireWhen.HasNoExpiry:
app.MapPost("/books/{bookId:int}/views", async (int bookId, IConnectionMultiplexer redis, TimeProvider clock) =>
{
IDatabase db = redis.GetDatabase();
RedisKey daily = RedisKeys.BookViewsDaily(bookId, DateOnly.FromDateTime(clock.GetUtcNow().UtcDateTime));
// Üç komut birlikte gönderilir (pipeline) ve birlikte beklenir
Task<long> total = db.StringIncrementAsync(RedisKeys.BookViews(bookId));
Task<long> today = db.StringIncrementAsync(daily);
Task<bool> ttl = db.KeyExpireAsync(daily, TimeSpan.FromDays(8), ExpireWhen.HasNoExpiry); // EXPIRE ... NX
await Task.WhenAll(total, today, ttl);
return Results.Ok(new { bookId, views = total.Result, today = today.Result });
});
curl -X POST http://localhost:5080/books/42/views
{"bookId":42,"views":1,"today":1}
curl -X POST http://localhost:5080/books/42/views
{"bookId":42,"views":2,"today":2}
docker compose -f lab/compose.yaml exec redis redis-cli TTL bookstore:book:42:views:20261114
(integer) 691200
691.200 saniye, yani 8 gün; ikinci istek süreyi sıfırlamadı. Üç komut tek round trip'te gidiyor, ama birlikte atomik değil: INCR ile EXPIRE arasında uygulama çökerse süresiz bir günlük key kalabilir. Bir sonraki EXPIRE … NX o key'e süreyi yine verir, yani gün içinde trafik devam ettiği sürece sorun kendini düzeltir. Bunu tek adımda yapmanın yollarını (MULTI/EXEC, Lua ve Redis 8.8'in INCREX'i) 08'de göreceğiz.
GET + SET: Kaybolan 9.617 Artış
Atomik komutun neden önemli olduğunu tersinden görelim. Aynı sayacı bu kez client'ta okuyup bir ekleyip geri yazıyoruz; 50 eşzamanlı çağıran, her biri 200 kez:
Task[] callers = [.. Enumerable.Range(0, 50).Select(_ => Task.Run(async () =>
{
for (int i = 0; i < 200; i++)
{
long current = (long)await db.StringGetAsync(key);
await db.StringSetAsync(key, current + 1);
}
}))];
await Task.WhenAll(callers);
Console.WriteLine($"expected 10000, got {await db.StringGetAsync(key)}");
expected 10000, got 383
expected 10000, got 382
expected 10000, got 383
10.000 artışın 9.617'si kayboldu. Her GET ve SET kendi içinde atomik, ama ikisinin arası değil: 50 çağıran aynı değeri okuyor, hepsi aynı sayıyı bir artırıp yazıyor. Multiplexing bu etkiyi büyütüyor, çünkü 50 GET aynı pipeline'da art arda gidip aynı cevabı alıyor. Redis'te "oku, hesapla, yaz" mantığı gördüğünde ilk soru şu olmalı: bunu sunucuda tek komutla yapabilir miyim? Sayaçta cevap INCR. "Değer hâlâ beklediğim gibiyse yaz" mantığında cevap aşağıdaki compare-and-set.
Compare-and-Set: SET IFEQ, DELEX ve DIGEST
Redis 8.4'ten önce "değer X ise sil" ya da "değer X ise değiştir" gibi bir işlemi atomik yapmanın yolu Lua script'iydi. Lock'u yalnız sahibi bıraksın diye yıllarca şu script kullanıldı; KEYS[1] lock key'i, ARGV[1] lock'u alırken yazılan token:
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
Redis 8.4 bunu komut seviyesine taşıdı. SET'e IFEQ, IFNE, IFDEQ, IFDNE koşulları geldi; koşullu silme için DELEX, value'nun hash'ini almak için DIGEST eklendi:
127.0.0.1:6379> SET bookstore:idem:7f3c9a processing NX EX 86400
OK
127.0.0.1:6379> SET bookstore:idem:7f3c9a order:1001 IFEQ processing KEEPTTL
OK
127.0.0.1:6379> SET bookstore:idem:7f3c9a order:1002 IFEQ processing KEEPTTL
(nil)
127.0.0.1:6379> GET bookstore:idem:7f3c9a
"order:1001"
127.0.0.1:6379> DIGEST bookstore:idem:7f3c9a
"967bf420e1a321ef"
127.0.0.1:6379> SET bookstore:verify:7 482913 EX 300
OK
127.0.0.1:6379> DELEX bookstore:verify:7 IFEQ 111111
(integer) 0
127.0.0.1:6379> DELEX bookstore:verify:7 IFEQ 482913
(integer) 1
127.0.0.1:6379> DELEX bookstore:verify:7 IFEQ 482913
(integer) 0
IFEQ'li ikinci SET değer artık processing olmadığı için yazmadı. DELEX … IFEQ yalnız değer eşitse siliyor ve kaç key sildiğini döndürüyor. DIGEST, value'nun XXH3 (hızlı, kriptografik olmayan bir hash algoritması) hash'ini hex olarak verir; IFDEQ/IFDNE ile kullanılır. Değer büyükse (örneğin birkaç yüz KB'lık bir JSON) karşılaştırma için bütün değeri göndermek yerine 16 karakterlik digest'i gönderirsin. StackExchange.Redis bu koşulları ValueCondition tipiyle sunuyor: ValueCondition.Equal(...), NotEqual, DigestEqual; digest'i client tarafında hesaplayan bir yardımcı da var.
Valkey farkı burada belirgin. Valkey 8.1 SET … IFEQ'i ekledi ve 9.0 koşullu silme için DELIFEQ key value'yu getirdi; ama IFNE, IFDEQ, IFDNE, DELEX ve DIGEST yok. docker compose -f lab/compose.yaml exec valkey valkey-cli ile:
127.0.0.1:6379> SET bookstore:lock:report token-a
OK
127.0.0.1:6379> DELIFEQ bookstore:lock:report token-b
(integer) 0
127.0.0.1:6379> DELIFEQ bookstore:lock:report token-a
(integer) 1
127.0.0.1:6379> DIGEST bookstore:idem:7f3c9a
(error) ERR unknown command 'DIGEST', with args beginning with: 'bookstore:idem:7f3c9a'
Bookstore: Tek Kullanımlık Doğrulama Kodu
Önce Tahmin Et'teki doğrulama kodunu doğru yazalım. Kod oluşturulurken her seferinde 5 dakikalık süreyle yazılıyor; doğrulama ise DELEX … IFEQ ile tek adım: kod doğruysa silinip başarı dönüyor, yanlışsa ya da süresi dolmuşsa hiçbir şey değişmiyor:
app.MapPost("/customers/{customerId:int}/verification-code",
async (int customerId, IConnectionMultiplexer redis, ILoggerFactory loggers) =>
{
string code = RandomNumberGenerator.GetInt32(100_000, 1_000_000).ToString();
// Yeni kod eskisinin yerine geçer, 5 dakikalık süresiyle birlikte
await redis.GetDatabase().StringSetAsync(RedisKeys.VerificationCode(customerId), code, CodeLifetime);
loggers.CreateLogger("Bookstore.Verification").LogInformation("Verification code for customer {CustomerId}: {Code} (would be e-mailed)", customerId, code);
return Results.Accepted(value: new { expiresInSeconds = (int)CodeLifetime.TotalSeconds });
});
app.MapPost("/customers/{customerId:int}/verification-code/verify",
async (int customerId, VerifyRequest request, IConnectionMultiplexer redis) =>
{
// DELEX key IFEQ code: yalnız kayıtlı kod eşleşirse siler; tek atomik adım (Redis 8.4+)
bool verified = await redis.GetDatabase()
.StringDeleteAsync(RedisKeys.VerificationCode(customerId), ValueCondition.Equal(request.Code));
return verified
? Results.Ok(new { verified })
: Results.BadRequest(new { verified, error = "invalid or expired code" });
});
curl -X POST http://localhost:5080/customers/7/verification-code
{"expiresInSeconds":300}
info: Bookstore.Verification[0]
Verification code for customer 7: 713745 (would be e-mailed)
curl -s -w " %{http_code}" -X POST http://localhost:5080/customers/7/verification-code/verify -H "Content-Type: application/json" -d '{"code":"111111"}'
{"verified":false,"error":"invalid or expired code"} 400
curl -s -w " %{http_code}" -X POST http://localhost:5080/customers/7/verification-code/verify -H "Content-Type: application/json" -d '{"code":"713745"}'
{"verified":true} 200
curl -s -w " %{http_code}" -X POST http://localhost:5080/customers/7/verification-code/verify -H "Content-Type: application/json" -d '{"code":"713745"}'
{"verified":false,"error":"invalid or expired code"} 400
Aynı kod ikinci kez kullanılamıyor. GET sonra DEL ile yazsaydık, aynı kodla gelen iki eşzamanlı istek ikisi de GET'ten doğru cevap alıp ikisi de başarılı sayılabilirdi. GETDEL de bir seçenek, ama yanlış bir tahmin kodu silerdi ve kullanıcı bir yazım hatası yüzünden yeni kod istemek zorunda kalırdı. Bir eksik bilerek bırakıldı: deneme sayısı sınırsız ve 6 haneli kod tahminle bulunabilir. Doğrulama endpoint'ine rate limit gerekir; tasarımı ASP.NET Core Güvenliği serisinin "Rate Limiting Derinlemesine" yazısında.
Aynı API Valkey'e bağlanınca doğrulama endpoint'i 500 döndü: StackExchange.Redis.RedisServerException: ERR unknown command 'DELEX'. Valkey'i destekleyeceksen bu adımı DELIFEQ ile (ExecuteAsync üstünden) ya da Lua script'iyle yazman gerekir.
Bookstore: Sipariş için Idempotency Key
Ödeme sayfasında çift tıklama ya da client'ın retry'ı aynı siparişi iki kez oluşturmasın diye client her siparişe bir Idempotency-Key header'ı ekliyor. Konunun tasarım tarafı Çözüm Desenleri serisinin "Idempotency Key: Çift Tıklama ve Retry'da Aynı Siparişi İki Kez Oluşturmamak" yazısında; burada Redis tarafını kuruyoruz. İlk istek key'i processing değeriyle NX ile alıyor, sipariş oluşunca sonucu IFEQ processing KEEPTTL ile yazıyor:
IDatabase db = redis.GetDatabase();
RedisKey key = RedisKeys.Idempotency(idempotencyKey);
// SET key processing NX EX 86400: yalnız ilk istek geçer
bool first = await db.StringSetAsync(key, Processing, IdempotencyWindow, When.NotExists);
if (!first)
{
RedisValue stored = await db.StringGetAsync(key);
return stored == Processing
? Results.Conflict(new { error = "a request with this key is still being processed" })
: Results.Ok(new { orderId = (long)stored, replayed = true });
}
try
{
long orderId = await db.StringIncrementAsync(RedisKeys.OrderIdSequence); // gerçek sipariş kaydının yerine geçiyor
// SET key <orderId> IFEQ processing KEEPTTL: sonucu yalnız key hâlâ bizimse yaz
await db.StringSetAsync(key, orderId, Expiration.KeepTtl, ValueCondition.Equal(Processing));
return Results.Created($"/orders/{orderId}", new { orderId, replayed = false });
}
catch
{
// Client tekrar deneyebilsin diye key'i bırak; yalnız hâlâ bizimse
await db.StringDeleteAsync(key, ValueCondition.Equal(Processing));
throw;
}
curl -s -w " %{http_code}" -X POST http://localhost:5080/orders -H "Content-Type: application/json" -H "Idempotency-Key: 7f3c9a" -d '{"customerId":7,"isbn":"9780441013593","quantity":1}'
{"orderId":1,"replayed":false} 201
curl -s -w " %{http_code}" -X POST http://localhost:5080/orders -H "Content-Type: application/json" -H "Idempotency-Key: 7f3c9a" -d '{"customerId":7,"isbn":"9780441013593","quantity":1}'
{"orderId":1,"replayed":true} 200
curl -s -w " %{http_code}" -X POST http://localhost:5080/orders -H "Content-Type: application/json" -H "Idempotency-Key: 8b21d0" -d '{"customerId":7,"isbn":"9780441013593","quantity":1}'
{"orderId":2,"replayed":false} 201
curl -s -w " %{http_code}" -X POST http://localhost:5080/orders -H "Content-Type: application/json" -H "Idempotency-Key: c0ffee" -d '{"customerId":7,"isbn":"9780441013593","quantity":1}'
{"error":"a request with this key is still being processed"} 409
İkinci istek yeni sipariş oluşturmadı, ilkinin sonucunu döndürdü. Son istekten önce bookstore:idem:c0ffee key'ini redis-cli ile elle processing değerine çektim, yani o anda işlenen bir isteği taklit ettim; böyle bir key için 409 dönüyor. KEEPTTL sayesinde sonuç yazılırken 24 saatlik pencere sıfırlanmıyor. IFEQ koşulu da önemli: key süresi dolup başka bir istek tarafından yeniden alındıysa, geç kalan ilk istek onun üzerine yazmıyor. Gerçek uygulamada sipariş SQL Server'a yazılır ve bu iki adım arasında süreç çökebilir; o durumda key 24 saat processing'de kalır. Bunun için processing durumuna kısa bir süre (örneğin 30 saniye) verip sonucu yazarken süreyi uzatmak ya da kalıcı kaydı veritabanında tutmak gerekir; Message Queue'larla Çalışmak serisinin "Idempotent Consumer ve Inbox: Aynı Mesaj İki Kez Geldiğinde" yazısındaki inbox tablosu bu ikinci yol.
Expiration'ın İçi
Redis bir key'in süresini kalan saniye olarak değil, mutlak bir Unix zaman damgası (milisaniye) olarak saklar. EXPIRETIME ve PEXPIRETIME bunu doğrudan gösterir:
127.0.0.1:6379> SET k v
OK
127.0.0.1:6379> EXPIRE k 100
(integer) 1
127.0.0.1:6379> EXPIRETIME k
(integer) 1794652280
127.0.0.1:6379> PEXPIRETIME k
(integer) 1794652279939
127.0.0.1:6379> TTL nokey
(integer) -2
127.0.0.1:6379> PERSIST k
(integer) 1
127.0.0.1:6379> TTL k
(integer) -1
-2 key'in olmadığını, -1 süresiz olduğunu gösterir. Mutlak zaman damgasının iki sonucu var. Birincisi, Redis kapalıyken de zaman akar: bir RDB (Redis'in belli aralıklarla aldığı snapshot dosyası) dosyasını yüklediğinde süresi geçmiş key'ler yüklenirken silinir. İkincisi, süre makinenin saatine bağlıdır. Redis dokümanı açıkça yazıyor: çalışan bir sunucunun saatini ileri alırsan TTL'i dolmamış key'ler hemen silinebilir; saatleri çok farklı iki makine arasında RDB taşırsan da beklenmedik sonuçlar çıkar. Sunucu saatini NTP (Network Time Protocol; saatleri ağ üzerinden eşitleyen protokol) ile düzgün tutmak bu yüzden Redis için de önemli.
Süresi dolan key iki yolla silinir. Pasif (lazy) yolda bir client key'e erişmeye çalıştığında Redis süresine bakar, dolmuşsa siler ve key yokmuş gibi cevap verir. Ama hiç okunmayan key'ler de var; onlar için aktif yol çalışır: Redis düzenli aralıklarla süreli key'lerden rastgele örnekler alır ve süresi dolanları siler. Etkisini 200.000 doğrulama kodunu 1–2 saniyelik sürelerle yazıp sonra hiç okumayarak ölçtüm:
t= 0.0s DBSIZE=200,000 expired_keys= 0
t= 1.0s DBSIZE=199,997 expired_keys= 3
t= 2.0s DBSIZE= 181 expired_keys=199,954
t= 3.0s DBSIZE= 0 expired_keys=200,000
Hiçbir client okumadan, süreler dolduktan kısa süre sonra key'lerin hepsi bellekten çıktı. Aktif temizliğin ne sıklıkla ve ne kadar CPU harcayarak çalışacağını hz (varsayılan 10) ve active-expire-effort (varsayılan 1) ayarları belirler; çok sayıda key'in aynı anda dolduğu sistemlerde bunlar INFO stats'taki expired_keys ve expired_stale_perc ile birlikte izlenir.
Replication'da kural farklı: replica'lar key'leri kendi başına silmez. Süre primary'de dolunca primary bir DEL üretir ve bunu hem replica'lara hem AOF (Append Only File; Redis'in yazma komutlarını sırayla kaydettiği kalıcılık dosyası) dosyasına gönderir. Böylece silme kararı tek yerde verilir. Replica primary olursa süreli key'leri kendisi silmeye başlar. Replication ayrıntıları 13'te.
Üretimde Dikkat
Her yazı yolunda TTL'in ne olacağına bilerek karar ver: yeni süre mi (EX), eskisi mi (KEEPTTL), hiç mi. Cache dışındaki string'lerde de süresiz key birikimi bellek sızıntısıdır; INFO keyspace'teki keys ve expires oranını izle. Sayaçlar için kayıp kabul edilebiliyorsa FireAndForget (02) ve pipelining kullan; kabul edilemiyorsa sonucu bekle. Compare-and-set komutlarını kullanıyorsan minimum sunucu sürümün 8.4'tür; managed bir servis ya da Valkey kullanıyorsan önce hangi komutların desteklendiğine bak. Expiration'a güvenen her özellik sunucu saatinin doğru olmasına da güveniyor.
Sık Düşülen Tuzaklar
Üzerine yazarken TTL'i kaybetmek: Önce Tahmin Et'teki SET süreyi siler; doğrulama kodu sonsuza kadar geçerli kalır.
GET + SET ile read-modify-write: 10.000 artışın 9.617'si kayboldu. INCR, SET … IFEQ ya da 08'deki atomik araçları kullan.
Pencereli sayaçta her artışta EXPIRE: 60 saniyelik bir pencere için her INCR'den sonra EXPIRE 60 çağrılırsa pencere hiç kapanmaz. İki saniyede bir istek gelen bir müşteri için iki yolu karşılaştırdım:
t=0s always: count=1 ttl=60 NX: count=1 ttl=60
t=2s always: count=2 ttl=60 NX: count=2 ttl=58
t=4s always: count=3 ttl=60 NX: count=3 ttl=56
t=6s always: count=4 ttl=60 NX: count=4 ttl=54
Her seferinde EXPIRE çağrılan sayacın süresi hep 60'a dönüyor; trafik kesilmedikçe sayaç sıfırlanmıyor ve müşteri bir süre sonra haksız yere limite takılıyor. EXPIRE … NX süreyi yalnız ilk seferde veriyor.
Valkey'de Redis 8.4 komutlarını varsaymak: DELEX ve DIGEST yok, SET'in koşullarından yalnız IFEQ var.
INCRBYFLOAT ile para hesabı: sonuç kayan noktalı sayının hassasiyetiyle sınırlı; tutarları tam sayı olarak tut.
Küçük Bir Deneme
★☆☆ lost-update deneyindeki GET + SET çiftini StringIncrementAsync ile değiştir ve tekrar çalıştır. Sonra yalnız bir çağıranla (50 yerine 1) eski hâlini dene: neden bu kez kayıp yok?
★★☆ Doğrulama endpoint'ini kod yerine digest ile karşılaştıracak şekilde değiştir: kodu kaydederken değil, doğrularken ValueCondition.DigestEqual kullan. Redis tarafında hangi komutun gittiğini SLOWLOG yerine INFO commandstats ile doğrula.
★★★ Idempotency akışına processing durumu için 30 saniyelik süre ekle ve sonuç yazılırken süreyi 24 saate çıkar. Bunu tek komutla yapabiliyor musun (SET … IFEQ … EX)? Valkey'de aynı komut çalışıyor mu?
Bir sonraki yazıda hash'lere geçiyoruz: bir object'i field'lara bölerek saklamak, Redis 7.4 ve 8.0'ın field bazında expiration komutları, listpack ile hashtable arasındaki bellek farkı ve Bookstore'un sepeti.