Mert Özen Her Satırda Daha İleri
Veritabanı Redis — #07

Özel Data Type'lar: Bitmap, HyperLogLog, Geospatial ve Probabilistic Yapılar (Bloom, Cuckoo, Top-K)

Mert Özen 14 Kas 2026 15 dk 10 görüntülenme
Özel Data Type'lar: Bitmap, HyperLogLog, Geospatial ve Probabilistic Yapılar (Bloom, Cuckoo, Top-K)

Tekil sayımda bitmap, kimlikler sıkıysa kesin sonucu set'in elli birde biri bellekle verir; HyperLogLog ise kimlik ne olursa olsun 12 KB'ta yaklaşık yüzde 1 hatayla sayar. Gerçek çıktılarla aynı ziyaretçi verisinde set, bitmap ve HLL karşılaştırması, seyrek bitmap'in 14 MB'lık tuzağı, BITFIELD, en yakın depo için Geo, Bloom filter'ın ölçülen belleği ve false positive oranı, Cuckoo, Top-K, Count-Min Sketch, t-digest, Redis 8.8'in Array'i ve Valkey farkları.

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

05'te Bookstore'un (sitedeki serilerin ortak örnek uygulaması, bir online kitapçı) günlük tekil ziyaretçilerini her gün için bir set'te tuttuk: 50.000 müşterilik havuzdan günde 20.000 ziyaret, bir günün set'i 16.429 üye ve yaklaşık 622 KB. Aynı veriyi, aynı rastgele sayı tohumuyla, üç yapıya birden yazalım: set, müşteri numarası başına bir bit tutan bir bitmap ve HyperLogLog (HLL):

int[] customerIds = [.. Enumerable.Range(0, 20_000).Select(_ => random.Next(1, 50_001))];
await db.SetAddAsync($"bookstore:visitors:{day}", [.. customerIds.Select(id => (RedisValue)id)]);  // SADD
await db.HyperLogLogAddAsync($"bookstore:visitors:hll:{day}", [.. customerIds.Select(id => (RedisValue)id)]); // PFADD
IBatch batch = db.CreateBatch();
Task[] bits = [.. customerIds.Select(id => batch.StringSetBitAsync($"bookstore:active:{day}", id, true))]; // SETBIT
batch.Execute();
await Task.WhenAll(bits);

Bitmap ve HLL bir günü kaç byte'la tutar? Hangisi kesin sayar, hangisi yaklaşık? Yedi günün tekil ziyaretçisi 05'te SUNIONCARD ile 46.925 çıkmıştı; diğer ikisi ne der?

dotnet run --project src/Bookstore.RedisLab -- unique-visitors
one day (20261108):
  set     16,429 visitors    636,423 bytes
  bitmap  16,429 visitors     12,332 bytes
  hll     16,473 visitors     14,386 bytes
7 days:
  SUNIONCARD           46,925
  BITOP OR + BITCOUNT  46,925
  PFCOUNT (7 keys)     47,131  error 0.44 %
  every day: BITOP AND + BITCOUNT 20, SINTERCARD 20

Bitmap aynı kesin sonucu set'in elli birde biri kadar bellekle verdi; HLL ise 44 fazla saydı ve haftalıkta yüzde 0,44 yanıldı. Dikkatli bakarsan 47.131'i tanıyacaksın: 05'teki SUNIONCARD … APPROX da tam bu sayıyı vermişti, çünkü o seçenek de içeride HyperLogLog kullanıyor. Bu yazıda kesinlikten bir miktar vazgeçerek ya da verinin şeklinden yararlanarak bellek kazandıran yapıları anlatıyoruz: bitmap ve BITFIELD, HyperLogLog, en yakın depo için Geo, Redis 8 ile çekirdeğe giren Bloom, Cuckoo, Count-Min Sketch, Top-K ve t-digest, son olarak 8.8'in yeni Array tipi.

Bitmap: Müşteri Başına Bir Bit

Redis'te ayrı bir bitmap tipi yok; bitmap, bit seviyesinde okunup yazılan bir string. SETBIT key offset 1 o offset'teki biti 1 yapar ve gerekiyorsa string'i o offset'e kadar büyütür:

127.0.0.1:6379> SETBIT bookstore:active:20261114 7 1
(integer) 0
127.0.0.1:6379> SETBIT bookstore:active:20261114 42 1
(integer) 0
127.0.0.1:6379> GETBIT bookstore:active:20261114 42
(integer) 1
127.0.0.1:6379> GETBIT bookstore:active:20261114 43
(integer) 0
127.0.0.1:6379> BITCOUNT bookstore:active:20261114
(integer) 2
127.0.0.1:6379> BITPOS bookstore:active:20261114 1
(integer) 7
127.0.0.1:6379> STRLEN bookstore:active:20261114
(integer) 6
127.0.0.1:6379> SETBIT bookstore:active:20261114 100000000 1
(integer) 0
127.0.0.1:6379> STRLEN bookstore:active:20261114
(integer) 12500001
127.0.0.1:6379> MEMORY USAGE bookstore:active:20261114
(integer) 14680108

Müşteri 7 ve 42 bugün geldi; BITCOUNT 1 olan bitleri sayıyor, BITPOS ilk 1'in yerini buluyor. SETBIT'in dönüş değeri bitin eski hâli: 0 ise müşteri bugün ilk kez geliyor demek, 05'teki SADD dönüşü gibi. 42 numaralı bit için string 6 byte'a büyüdü. Sonra müşteri numarası 100.000.000 olan biri geldi ve string bir anda 12,5 milyon byte'a, Redis'in ayırdığı bellekle 14 MB'a çıktı. Bitmap'in bütün kazancı numaraların sıkı (dense) olmasına bağlı: 1'den 50.000'e kadar numaralarda bir gün 12 KB, aynı müşteri sayısına ama dağınık ve büyük numaralara sahip bir sistemde megabaytlar. Kimlikler GUID (rastgele üretilen 128 bitlik kimlik) ya da büyük, seyrek sayılarsa bitmap'i kullanma ya da önce kimliği sıkı bir sıra numarasına çevir.

Asıl güç günler arasındaki işlemlerde. BITOP OR yedi günün bitmap'ini birleştirip "hafta içinde en az bir kez gelenleri", BITOP AND "her gün gelenleri" veriyor; Önce Tahmin Et'te ikisi de set'lerle birebir aynı sonucu verdi (46.925 ve 20). BITOP'un maliyeti bitmap'lerin uzunluğuyla orantılı, üye sayısıyla değil; seyrek ve uzun bitmap'lerde bunu da hesaba kat. Bookstore.Api'de her ziyaret artık üç yere yazılıyor; set'i karşılaştırma için 05'ten bırakıyoruz:

// Tek ziyaret, üç yapı: kesin set (05), müşteri numarası başına bit ve HyperLogLog (07)
app.MapPost("/visits", async (VisitRequest request, IConnectionMultiplexer redis, TimeProvider clock) =>
{
    IDatabase db = redis.GetDatabase();
    DateOnly day = DateOnly.FromDateTime(clock.GetUtcNow().UtcDateTime);
    RedisKey set = RedisKeys.DailyVisitors(day), bitmap = RedisKeys.ActiveCustomers(day), hll = RedisKeys.DailyVisitorsHll(day);

    Task<bool> added = db.SetAddAsync(set, request.CustomerId);                     // SADD
    Task<bool> wasActive = db.StringSetBitAsync(bitmap, request.CustomerId, true);   // SETBIT: bitin eski hâlini döndürür
    Task<bool> hllChanged = db.HyperLogLogAddAsync(hll, request.CustomerId);         // PFADD
    Task[] ttl = [.. new[] { set, bitmap, hll }.Select(k => db.KeyExpireAsync(k, Retention, ExpireWhen.HasNoExpiry))];
    await Task.WhenAll([added, wasActive, hllChanged, .. ttl]);
    return Results.Ok(new { firstVisitToday = added.Result, wasActiveBefore = wasActive.Result });
});
curl -X POST http://localhost:5080/visits -H "Content-Type: application/json" -d '{"customerId":7}'
{"firstVisitToday":true,"wasActiveBefore":false}
curl -X POST http://localhost:5080/visits -H "Content-Type: application/json" -d '{"customerId":7}'
{"firstVisitToday":false,"wasActiveBefore":true}
curl -X POST http://localhost:5080/visits -H "Content-Type: application/json" -d '{"customerId":42}'
{"firstVisitToday":true,"wasActiveBefore":false}
curl http://localhost:5080/visits/2026-11-14/active
{"day":"2026-11-14","activeCustomers":2}

Üç key'in hepsi 8 günlük TTL (key'in kalan yaşam süresi) alıyor; 05'teki gibi EXPIRE … NX ile yalnız ilk yazımda.

BITFIELD: Küçük Sayaçları Tek Key'de Tutmak

BITFIELD bir string'i istediğin genişlikte tam sayılardan oluşan bir dizi gibi kullanmanı sağlıyor. Bookstore'da her kitabın 1'den 5'e yıldız dağılımını tutmak için beş ayrı key ya da bir hash yerine 20 byte'lık tek bir string yeter:

127.0.0.1:6379> BITFIELD bookstore:book:9780441013593:ratings INCRBY u32 #4 1 INCRBY u32 #4 1 INCRBY u32 #2 1
1) (integer) 1
2) (integer) 2
3) (integer) 1
127.0.0.1:6379> BITFIELD bookstore:book:9780441013593:ratings GET u32 #0 GET u32 #1 GET u32 #2 GET u32 #3 GET u32 #4
1) (integer) 0
2) (integer) 0
3) (integer) 1
4) (integer) 0
5) (integer) 2
127.0.0.1:6379> STRLEN bookstore:book:9780441013593:ratings
(integer) 20

u32 32 bit işaretsiz tam sayı, #4 "dördüncü u32", yani 128. bitten başlayan alan. Dune (9780441013593) iki tane 5 yıldız ve bir tane 3 yıldız aldı. Tek komutta birden fazla işlem atomik olarak çalışıyor; OVERFLOW WRAP|SAT|FAIL ile sayaç taştığında ne olacağını da seçebiliyorsun. Bir milyon kitapta bunun bir hash'e göre bellek farkı ciddi, ama okunabilirlik düşük: hangi offset'in hangi yıldız olduğunu kod bilmek zorunda.

HyperLogLog: 12 KB'ta Tekil Sayım

HyperLogLog, gördüğü elemanları saklamadan "kaç farklı eleman gördüm?" sorusunu yaklaşık cevaplayan bir algoritma. Her elemanın hash'ine bakıp bir dizi küçük sayaç (register) günceller; elemanın kendisi hiçbir yerde durmaz, bu yüzden "müşteri 7 geldi mi?" sorusunu cevaplayamaz. Redis dokümanına göre Redis'teki HLL en fazla 12 KB kullanıyor ve standart hatası yüzde 0,81. Ölçümde iki farklı sayı gördüm: HLL'in kendisi (STRLEN) 12.304 byte, yani dokümandaki 12 KB artı 16 byte'lık başlık; MEMORY USAGE ise 14.386 byte gösterdi, çünkü bellek ayırıcı (jemalloc) 12.304 byte'ı daha büyük bir boyut sınıfından veriyor. Az eleman görmüş HLL ise sparse encoding'de çok daha küçük: 3 elemanlık bir HLL'in STRLEN'i 27 byte. Sparse'tan dense'e geçiş sınırını hll-sparse-max-bytes (3000) belirliyor.

Ölçeğin etkisini görmek için 100, 10.000 ve 1.000.000 farklı müşteriyi üç yapıya yazdım:

dotnet run --project src/Bookstore.RedisLab -- unique-scale
      100 customers  set          240 B   bitmap        50 B   hll    480 B  estimate       100 (+0.00 %)
   10,000 customers  set      350,056 B   bitmap     1,317 B   hll 14,370 B  estimate     9,988 (-0.12 %)
1,000,000 customers  set   32,277,596 B   bitmap   131,111 B   hll 14,372 B  estimate 1,009,972 (+1.00 %)

Set müşteri sayısıyla birlikte büyüyor ve bir milyonda 31 MB; HLL 10.000'den sonra sabit. Bir milyonda hata yüzde 1,00, yani dokümandaki yüzde 0,81'in biraz üstünde. Bu bir çelişki değil: yüzde 0,81 standart hata, tek bir ölçümün sapması bunun üstünde de çıkabilir; tahminlerin çoğu yüzde ±1,6'nın (iki standart hata) içinde kalır. Bu tabloda bitmap'in de iyi çıktığına dikkat et: numaralar 1'den başlayıp sıkı gittiği için bir milyon müşteri 128 KB ve sonuç kesin. Kimliklerin böyle bir yapısı varsa önce bitmap'i düşün; yoksa ya da bir müşteriden fazlasını (arama terimi, IP adresi, sayfa yolu) sayıyorsan HLL.

Birden fazla HLL'i sayarken iki yol var: PFCOUNT k1 k2 … k7 birleşimi geçici olarak hesaplayıp sayar, PFMERGE dest k1 … k7 birleşimi bir key'e yazar. Bookstore.Api'nin haftalık endpoint'i artık iki sayıyı birlikte döndürüyor:

// Son 7 günün tekil ziyaretçisi: kesin sayı SUNIONCARD (Redis 8.10+), tahmin PFCOUNT
Task<long> exact = db.SetCombineLengthAsync(SetOperation.Union, [.. days.Select(RedisKeys.DailyVisitors)]);
Task<long> estimated = db.HyperLogLogLengthAsync([.. days.Select(RedisKeys.DailyVisitorsHll)]); // PFCOUNT k1 ... k7
await Task.WhenAll(exact, estimated);
curl http://localhost:5080/visits/weekly
{"from":"2026-11-08","to":"2026-11-14","uniqueVisitors":2,"estimatedUniqueVisitors":2}

Küçük sayılarda HLL kesin sonuç veriyor; fark ancak binler seviyesinde ortaya çıkıyor. Yaklaşık sayımın bedeli de var: HLL'den eleman silinemez ve "bu müşteri sayıldı mı?" sorusu sorulamaz.

Geo: En Yakın Depo

Bookstore'un üç deposu var (koordinatlar temsili). Sipariş adresine en yakın depoyu bulmak, kargo süresi tahmininin ilk adımı:

127.0.0.1:6379> GEOADD bookstore:warehouses 28.9784 41.0082 istanbul 32.8597 39.9334 ankara 27.1428 38.4237 izmir
(integer) 3
127.0.0.1:6379> GEOSEARCH bookstore:warehouses FROMLONLAT 29.0610 40.1885 BYRADIUS 400 km ASC WITHDIST COUNT 2
1) 1) "istanbul"
   2) "91.4387"
2) 1) "izmir"
   2) "256.4705"
127.0.0.1:6379> GEOSEARCH bookstore:warehouses FROMLONLAT 30.5206 39.7767 BYRADIUS 400 km ASC WITHDIST
1) 1) "istanbul"
   2) "189.2814"
2) 1) "ankara"
   2) "200.4772"
3) 1) "izmir"
   2) "328.0742"
127.0.0.1:6379> GEODIST bookstore:warehouses istanbul ankara km
"349.4544"
127.0.0.1:6379> GEOPOS bookstore:warehouses izmir
1) 1) "27.142799198627472"
   2) "38.42369991211598"
127.0.0.1:6379> TYPE bookstore:warehouses
zset
127.0.0.1:6379> ZSCORE bookstore:warehouses istanbul
"3508663909312994"

Koordinatlar önce boylam, sonra enlem sırasıyla yazılıyor; tersini yazmak en sık hata. Bursa'daki bir adres için İstanbul 91 km ile en yakın. Eskişehir'deki bir adres için cevap İstanbul (189 km), Ankara (200 km) değil: Redis kuş uçuşu mesafe hesaplıyor, karayolu mesafesi ya da kargo rotası bilmiyor. Geri okunan İzmir koordinatının virgülden sonraki altıncı basamakta değişmesi de bir ipucu. Geo ayrı bir tip değil, bir sorted set: her nokta 52 bitlik bir geohash'e (koordinatı tek sayıya kodlayan yöntem) çevrilip skor olarak saklanıyor. Bu yüzden TYPE zset diyor, 06'daki bütün sorted set komutları (ZREM, ZCARD) burada da çalışıyor ve konum santimetreler mertebesinde yuvarlanıyor. Bookstore.Api'deki endpoint:

// Teslimat adresine en yakın depo: GEOSEARCH ... BYRADIUS ... ASC COUNT 1 WITHDIST
app.MapGet("/warehouses/nearest", async (double lon, double lat, IConnectionMultiplexer redis, double radiusKm = 400) =>
{
    GeoRadiusResult[] found = await redis.GetDatabase().GeoSearchAsync(RedisKeys.Warehouses, lon, lat,
        new GeoSearchCircle(radiusKm, GeoUnit.Kilometers), count: 1, demandClosest: true,
        order: Order.Ascending, options: GeoRadiusOptions.WithDistance);
    return found.Length == 0
        ? Results.NotFound(new { error = $"no warehouse within {radiusKm} km" })
        : Results.Ok(new { warehouse = found[0].Member.ToString(), distanceKm = found[0].Distance });
});
curl "http://localhost:5080/warehouses/nearest?lon=29.0610&lat=40.1885"
{"warehouse":"istanbul","distanceKm":91.4387}
curl -i "http://localhost:5080/warehouses/nearest?lon=43.3832&lat=38.5012"
HTTP/1.1 404 Not Found

{"error":"no warehouse within 400 km"}

demandClosest: true komuta ANY eklemiyor; Redis yarıçap içindeki bütün noktaları bulup sıralıyor ve en yakını döndürüyor. ANY (demandClosest: false) ilk bulduğu yeterli sayıda noktada durur ve büyük veri setlerinde daha hızlıdır, ama dönen nokta en yakın olmayabilir.

Bloom ve Cuckoo Filter

Redis 8 ile eskiden RedisBloom modülünde olan probabilistic yapılar çekirdek dağıtıma girdi; redis:8 image'ında ek kurulum gerekmiyor. Bloom filter "bu eleman kümede kesinlikle yok" ya da "muhtemelen var" der; yanlış "var" (false positive) olabilir, yanlış "yok" olamaz. Bookstore'da kullanım yeri belli: katalogda olmayan ISBN'lerle (kitabın 13 haneli uluslararası numarası) gelen istekler cache'te bulunamayıp her seferinde veritabanına gidiyor. Bu, Caching serisinin "Cache Stampede, Penetration ve Avalanche: Üretimde Cache'i Çökerten Üç Sorun" yazısındaki cache penetration sorunu; çözüm tarafı orada, burada Redis tarafının sayılarına bakalım. Bir milyon ISBN'i yüzde 1 hedef hatayla bir filtreye, aynı ISBN'leri bir set'e yazdım ve katalogda olmayan 100.000 ISBN'i sordum (BF.* komutları için StackExchange.Redis'te tipli bir API yok, ExecuteAsync ile gönderiliyor; tipli karşılıkları NRedisStack'te, 11'de kullanacağız):

await db.ExecuteAsync("BF.RESERVE", filter, 0.01, 1_000_000);
for (long start = 0; start < 1_000_000; start += 10_000)
{
    object[] args = [filter, .. Enumerable.Range(0, 10_000).Select(i => (object)Isbn(start + i))];
    await db.ExecuteAsync("BF.MADD", args);
}
dotnet run --project src/Bookstore.RedisLab -- probabilistic
bloom: 1,000,000 ISBNs in 1,378,616 bytes (set: 39,388,706 bytes), false positives 516 of 100,000 (0.52 %)

Filtre 1,3 MB, set 37,6 MB. Dokümandaki formüle göre yüzde 1 hata, eleman başına 9,585 bit ister; bir milyon eleman için bu 1,2 MB eder. Ölçtüğüm 1,38 MB daha fazla, false positive oranı da hedefin yarısı kadar: yüzde 0,52. İkisinin nedeni aynı ve BF.DEBUG'da görünüyor:

127.0.0.1:6379> BF.RESERVE bookstore:catalog:scaling 0.01 1000000
OK
127.0.0.1:6379> BF.DEBUG bookstore:catalog:scaling
1) "size:0"
2) "bytes:1378472 bits:11027776 hashes:8 hashwidth:64 capacity:1000000 size:0 ratio:0.005"
127.0.0.1:6379> BF.RESERVE bookstore:catalog:fixed 0.01 1000000 NONSCALING
OK
127.0.0.1:6379> BF.DEBUG bookstore:catalog:fixed
1) "size:0"
2) "bytes:1198136 bits:9585088 hashes:7 hashwidth:64 capacity:1000000 size:0 ratio:0.01"

Büyüyebilen (varsayılan) bir filtrede Redis ilk alt filtreyi istenen hatanın yarısıyla (ratio:0.005) kuruyor; kapasite aşılınca eklenecek alt filtrelerin hatası da toplama girecek, toplam hata hedefin altında kalsın diye. NONSCALING ile ayrılan bellek formülle birebir aynı: 9.585.088 bit. Kapasiteyi doğru vermek bu yüzden önemli: kapasite aşılınca Redis bir alt filtre daha ekliyor (varsayılan EXPANSION 2) ve her "yok" cevabı artık birden fazla filtreye bakarak veriliyor; NONSCALING bir filtrede ise kapasite aşılınca hata oranı büyümeye başlıyor. BF.RESERVE yapmadan doğrudan BF.ADD dersen filtre varsayılanlarla oluşuyor; BF.INFO'da kapasitesi 100 görünüyordu, yani büyük bir katalog için hemen alt filtre zinciri başlar.

Bloom filter'dan eleman silinemez. Silme gerekiyorsa Cuckoo filter var; örneğin "bu kupon kodu kullanıldı mı?" sorusunda iade edilen bir siparişin kuponunu yeniden açmak:

127.0.0.1:6379> CF.RESERVE bookstore:coupons:used 100000
OK
127.0.0.1:6379> CF.ADD bookstore:coupons:used WELCOME-7F3C
(integer) 1
127.0.0.1:6379> CF.EXISTS bookstore:coupons:used WELCOME-7F3C
(integer) 1
127.0.0.1:6379> CF.DEL bookstore:coupons:used WELCOME-7F3C
(integer) 1
127.0.0.1:6379> CF.EXISTS bookstore:coupons:used WELCOME-7F3C
(integer) 0

Dokümana göre Bloom ekleme tarafında, Cuckoo ise kontrol tarafında daha hızlı. Cuckoo'da silmeyi yalnız gerçekten eklediğin bir eleman için yap: hiç eklenmemiş ama filtrede "var" görünen bir elemanı silmek, başka bir elemanın izini silebilir. Kupon kullanımının asıl kaydı yine veritabanında; filtre yalnız gereksiz sorguyu azaltıyor.

Count-Min Sketch, Top-K ve t-digest

Aynı deneyin geri kalanı üç soruyu cevaplıyor. Hangi arama terimleri en çok aranıyor (Top-K)? Belirli bir terim kaç kez arandı (Count-Min Sketch, sayıları küçük bir sayaç tablosunda tutan ve her zaman fazla tahmin eden yapı)? Sipariş tutarlarının yüzde 95'lik dilimi kaç TL (t-digest, dağılımın yüzdelik değerlerini tahmin eden yapı)? 17.205 farklı terimden oluşan 100.000 arama ve 100.000 sipariş tutarı kullandım:

top-k (estimate / exact):
  dune     21,135 / 21,136
  hobbit    8,742 /  8,754
  tolkien   6,742 /  6,754
  austen    5,672 /  5,684
  orwell    4,973 /  4,982
count-min sketch (estimate / exact): dune 21,140 / 21,136, middlemarch 4,195 / 4,189, q42 13 / 2, never-searched 4 / 0
memory: top-k 2,696 B, cms 56,072 B, distinct terms 17,205
t-digest (estimate / exact): p50 147.60 / 147.81, p95 395.42 / 395.13, p99 600.93 / 601.11, 9,888 bytes

Top-K, 2,7 KB'la ilk beş terimi doğru sırayla buldu; sayıları gerçeğe çok yakın ama biraz eksik. Count-Min Sketch her zaman fazla sayar, hiç eksik saymaz: popüler terimlerde fark küçük (21.140'a karşı 21.136), seyrek terimlerde oransal olarak büyük (2 kez aranan q42 için 13), hiç aranmamış bir terim için bile 4. Bu yüzden "çok aranan terimleri bul" sorusuna uygun, "nadir terimleri say" sorusuna uygun değil. t-digest 100.000 tutarı 9,9 KB'ta tuttu ve yüzde 95'lik dilimi 395,42 TL olarak verdi; kesin değer 395,13. Yüzdelikler için bütün değerleri saklayıp sıralamak yerine t-digest, uç dilimlerde (p99) bile bir liranın altında bir hatayla çalışıyor. Bu yapıların hepsi tek tek ekleme ve sorgu komutları olan sıradan Redis key'leri; TTL verebilir, günlük key'lerle zaman pencereleri kurabilirsin.

Valkey Farkı ve Yeni Tipler

Valkey 9.1.3'te bitmap, BITFIELD, HyperLogLog ve Geo komutları Redis'tekiyle aynı çalıştı. Probabilistic yapılarda durum farklı: valkey/valkey:9 image'ında BF.ADD, CF.RESERVE, TOPK.RESERVE ve TDIGEST.CREATE ERR unknown command döndürdü. Valkey'de Bloom filter ayrı bir modülle, valkey-bloom ile geliyor (Valkey dokümanı 8.0 ve sonrası için resmî modül diyor; Docker'da valkey-bundle image'ı öneriliyor) ve BF.* komutlarıyla uyumlu. Cuckoo, Count-Min Sketch, Top-K ve t-digest'in Valkey dokümanında bir karşılığını bulamadım; bunları kullanan bir uygulama Valkey'e geçerken alternatif arar.

Redis 8.8 bir de Array tipi getirdi: index ile erişilen, seyrek olabilen bir string dizisi. Bitmap'teki tuzağı hatırla; aynı büyük index'i Array'e yazınca:

127.0.0.1:6379> ARSET bookstore:book:9780441013593:daily-sales 317 12
(integer) 1
127.0.0.1:6379> ARSET bookstore:book:9780441013593:daily-sales 100000000 1
(integer) 1
127.0.0.1:6379> ARGET bookstore:book:9780441013593:daily-sales 317
"12"
127.0.0.1:6379> ARLEN bookstore:book:9780441013593:daily-sales
(integer) 100000001
127.0.0.1:6379> ARCOUNT bookstore:book:9780441013593:daily-sales
(integer) 2
127.0.0.1:6379> MEMORY USAGE bookstore:book:9780441013593:daily-sales
(integer) 33165

Uzunluk 100 milyon görünüyor, ama yalnız iki dolu eleman var ve key 33 KB tutuyor; bitmap aynı index için 14 MB tutmuştu. ARRING ile sabit boyutlu bir ring buffer (dolunca en eskinin üzerine yazan dizi), AROP ile bir aralıkta toplama gibi işlemler de var. Tip çok yeni (8.8, Mayıs 2026) ve Valkey'de yok (ARSET için ERR unknown command); production'da kullanmadan önce client desteğini ve managed servisindeki Redis sürümünü kontrol et. Aynı dağıtımda Time Series tipi (TS.* komutları: zaman damgalı ölçümler, otomatik downsampling kuralları) ve vector set (VADD, VSIM) de var. Vector set'i bir vector store olarak kullanmak RAG (Retrieval-Augmented Generation; modele cevap üretmeden önce ilgili belgeleri bulup veren yaklaşım) serisinin "Vector Database Seçimi: pgvector, SQL Server 2025, Qdrant, Azure AI Search ve Elasticsearch" yazısının konusu.

Üretimde Dikkat

Hangi yapıyı seçtiğini bir cümleyle yazabilmelisin: "kesin sayı lazım, kimlikler sıkı, bitmap"; "yüzde 1 hata kabul, kimlik keyfi, HLL"; "yokluğu kesin bilmem yeter, Bloom". Belleği tahmin etmek için gerçek veriyle küçük bir deneme yap ve MEMORY USAGE'a bak; dokümandaki teorik değerler (HLL için 12 KB, Bloom için eleman başına 9,585 bit) ile MEMORY USAGE arasında bu yazıda yüzde 15–20 fark çıktı; nedenleri bellek ayırıcının boyut sınıfları ve büyüyebilen filtrenin sıkılaştırılmış hata oranı. Bitmap'te tek bir büyük offset key'i bir anda megabaytlara büyütür ve bu yazma sırasında sunucu o belleği ayırmak zorunda kalır; offset'i dışarıdan gelen bir değerden alıyorsan bir üst sınır koy. Probabilistic yapılarda kapasite ve hata oranı oluştururken veriliyor ve sonradan değiştirilemiyor; tahmin ettiğinden büyük çıkarsa yeni bir filtre oluşturup yeniden doldurman gerekir. Günlük key'lere TTL ver; 12 KB'lık HLL bile müşteri ya da sayfa başına günlük tutulursa birikir.

Sık Düşülen Tuzaklar

Yaklaşık yapıyı kesin sonuç gereken yerde kullanmak: faturalanan kullanıcı sayısı, stok, kupon limiti HLL ya da Bloom ile hesaplanmaz. Önce Tahmin Et'te HLL bir günde 44 kişi fazla saydı.

Seyrek ve büyük kimliklerle bitmap: 100.000.000 numaralı tek bir müşteri key'i 14 MB'a çıkardı.

Count-Min Sketch'i nadir olayları saymak için kullanmak: hiç aranmamış bir terim 4, iki kez aranan bir terim 13 göründü.

Bloom filter'ı kapasitesiz oluşturmak: BF.ADD'in oluşturduğu filtrenin kapasitesi 100.

Geo'da enlem ve boylamı ters yazmak ya da kuş uçuşu mesafeyi teslimat mesafesi sanmak.

Redis 8'in probabilistic komutlarını ve 8.8'in Array'ini Valkey'de ya da eski bir managed Redis'te varsaymak.

Küçük Bir Deneme

★☆☆ İki günün HLL'ini PFMERGE ile birleştir, sonucun PFCOUNT'unu iki key'i birlikte veren PFCOUNT k1 k2 ile karşılaştır. Aynı mı?

★★☆ unique-scale deneyini müşteri numaralarını 1'den değil 1.000.000.000'dan başlatarak tekrarla. Set, bitmap ve HLL'in belleği nasıl değişiyor?

★★★ Bloom filter'ı kapasite 1.000 ile oluştur ve bir milyon ISBN ekle. BF.INFO'daki alt filtre sayısı, bellek ve false positive oranı nasıl değişiyor? Aynı denemeyi NONSCALING ile yap: bu kez ne oluyor?

Bir sonraki yazıda atomikliğe geçiyoruz: MULTI/EXEC neden rollback yapmaz, WATCH ile optimistic concurrency, Lua script'leri ve Redis Functions; Bookstore'da stok rezervasyonunu tek atomik adımda yapacağız ve 06'daki snapshot hesaplamasını da tek adıma indireceğiz.