İçeriğe atla

Key-Value Store Nedir? Doğru Çözümü Seçme Rehberi

Key-value storage'a temel bir rehber: ne olduğu, nerede kullanıldığı, takımların neden seçtiği ve hangi tech stack'lerde hangi çözümlerin geldiği.

Ayhan Sipahi Ayhan Sipahi

Relational database pattern’lerini key-value erişim workload’larına (session storage, caching, sepet verisi) uygulamak önlenebilir gecikmeye ve şema karmaşıklığına yol açar. Yanlış storage modelini seçmek, takımları temel bir mimari uyumsuzluğu gideremeyen index ayarlama döngülerine sürükler. Key-value storage bu uyumsuzluğu ortadan kaldırır: veri modeli erişim pattern’iyle örtüşür, böylece bir lookup sorgu planı yerine tek bir hash probe’a iner.

Dağıtık workload’larda varsayılan Redis’tir; onu geçmek için somut bir gerekçe gerekir. İstisnalar dar ve tahmin edilebilir: configuration ve coordination için etcd, dalgalanan serverless trafik için DynamoDB, tek sunucu varsa in-process cache ve tüm deployment JVM üzerinde koşuyorsa gömülü Hazelcast.

”Sadece Database Kullan” Yanılgısı#

Tablo tanıdıktır. Session verisi MySQL’de durur, kullanıcı tercihleri ikinci bir tabloda yaşar ve her istek, baştan beri relational olmayan bir state’i yeniden kurmak için bu ikisini join’ler. Demo yükü altında response time’lar tırmanır; ilk refleks index eklemek ve connection pool’u genişletmektir.

Bu düzeltmeler biraz nefes aldırır, sonra tıkanır. Query planner her istekte gerçek iş yapıyordur: parse et, planla, index’leri gez, join’i materyalize et. “Şu session id’nin altındaki değeri ver” sorusunu yanıtlamak için bu işlerin hiçbiri gerekmez.

Key-Value Veri Modeli#

Key-value storage, veriyi benzersiz tanımlayıcılar (key’ler) ve bunlara bağlı değerler (value’lar) çiftleri olarak saklayan bir NoSQL database paradigmasıdır. Önceden tanımlanmış şemalar ve karmaşık ilişkilere sahip relational database’lerin aksine, KV store’lar hızlı erişim için optimize edilmiş basit, düz bir yapı kullanır.

// Temel Key-Value Konsepti
const keyValueStore = {
  "user:1001": {
    name: "John Doe",
    email: "john@example.com",
    lastLogin: "2024-01-15T10:30:00Z"
  },
  "session:abc123": {
    userId: 1001,
    expiresAt: 1642248600,
    permissions: ["read", "write"]
  },
  "cart:user:1001": [
    { productId: 501, quantity: 2 },
    { productId: 302, quantity: 1 }
  ]
};

// Erişim Pattern'i: O(1) lookup time
const userData = keyValueStore["user:1001"];
const sessionData = keyValueStore["session:abc123"];

Tasarım Gereği Schema-Free#

  • Schema-free: Value’lar her şey olabilir: string’ler, number’lar, JSON object’ler, binary data, array’ler
  • Basit İşlemler: Temel işlemler key ile GET, PUT, DELETE
  • Hızlı Erişim: Hash table’lar veya B-tree’ler kullanarak sub-millisecond key lookup’lar için optimize edilmiş
  • Esnek Value’lar: Karmaşık veri türlerinde (list’ler, set’ler, hash’ler) atomic işlem desteği

Temel farkı gösteren veri modeli karşılaştırması:

-- Relational Database (Karmaşık)
SELECT u.name, u.email, s.permissions
FROM users u
JOIN sessions s ON u.id = s.user_id
WHERE s.session_id = 'abc123';

-- Key-Value Store (Basit)
GET session:abc123
GET user:1001

Relational yaklaşım database’in sorgu planlaması, index bakımı ve join’leri yürütmesini gerektirir. Key-value yaklaşımı ise direkt bir hash table lookup’tur.

Yaygın Erişim Pattern’leri#

Aşağıdaki beş erişim pattern’i, takımların bir KV sisteminde sakladığı verinin büyük bölümünü kapsar.

1. Session Management#

En büyük kazanımlar genellikle burada görülür. E-commerce session storage key-value pattern’ler için mükemmel:

// E-commerce session storage
interface UserSession {
  userId: string;
  cartItems: CartItem[];
  preferences: UserPreferences;
  expiresAt: number;
}

// Key pattern: session:${sessionId}
const sessionKey = "session:abc123-def456-ghi789";
await kvStore.set(sessionKey, sessionData, { ttl: 3600 }); // 1 saatlik expiry

Yukarıdaki ttl seçeneği, session’ı bir saat sonra otomatik olarak sona erdirir.

2. Caching Layer#

Database query result caching, KV storage’ın en yaygın kullanım alanlarından biri:

# Database query result caching
import redis
import json

def get_user_profile(user_id):
    cache_key = f"user_profile:{user_id}"
    cached = redis_client.get(cache_key)

    if cached:
        return json.loads(cached)

    # Pahalı database sorgusu
    profile = database.query("SELECT * FROM users WHERE id = ?", user_id)
    redis_client.setex(cache_key, 300, json.dumps(profile))  # 5 dakika cache
    return profile

setex’teki beş dakikalık TTL, önbelleğe alınan profilin ne kadar bayatlayabileceğini sınırlar.

3. Real-time Analytics ve Counter’lar#

Counter’larda atomic işlemlere ihtiyaç duyan sistemler için:

// Real-time sayfa görüntüleme sayımı
public class PageViewCounter {
    private IMap<String, Long> pageViews;

    public void incrementPageView(String pageId) {
        String key = "pageviews:" + pageId;
        pageViews.merge(key, 1L, Long::sum);  // Atomic increment
    }

    public long getPageViews(String pageId) {
        return pageViews.getOrDefault("pageviews:" + pageId, 0L);
    }
}

Yukarıdaki atomic merge, ayrı bir read-modify-write’ın yol açacağı lost update sorununu önler.

4. Configuration Management#

etcd’nin watch semantiği, dinamik uygulama konfigürasyonunu doğal bir kullanım alanı yapar:

// Dynamic uygulama konfigürasyonu
type ConfigManager struct {
    client *clientv3.Client
}

func (c *ConfigManager) GetConfig(service string) (*Config, error) {
    key := fmt.Sprintf("/config/%s", service)
    resp, err := c.client.Get(context.Background(), key)
    if err != nil {
        return nil, err
    }

    var config Config
    json.Unmarshal(resp.Kvs[0].Value, &config)
    return &config, nil
}

5. Multi-Tier Caching Stratejisi#

Farklı storage tier’ların avantajlarını birleştiren hybrid yaklaşım:

// L1: In-memory cache (en hızlı, en küçük)
// L2: Distributed cache (Redis)
// L3: Database (en yavaş, persistent)

class MultiTierCache {
  async get(key) {
    // L1: In-memory kontrol
    let value = this.memoryCache.get(key);
    if (value) return value;

    // L2: Redis kontrol
    value = await this.redisClient.get(key);
    if (value) {
      this.memoryCache.set(key, value, 60); // 1 dakika L1 cache
      return JSON.parse(value);
    }

    // L3: Database sorgusu
    value = await this.database.query(key);
    if (value) {
      await this.redisClient.setex(key, 300, JSON.stringify(value)); // 5 dakika L2
      this.memoryCache.set(key, value, 60); // 1 dakika L1 cache
    }

    return value;
  }
}

Kod parçasındaki iki TTL, bir dakika bellekte ve beş dakika Redis’te, her katmanın ne kadar hızlı bayatlayacağını belirler.

Performance Gerekçesi#

Avantaj yapısaldır ve iki erişim yolunu yan yana koyunca görülür:

-- Relational: üç tabloya yayılan session lookup
SELECT u.name, u.email, p.theme, p.language, s.cart_items
FROM users u
JOIN user_preferences p ON u.id = p.user_id
JOIN user_sessions s ON u.id = s.user_id
WHERE s.session_id = 'abc123';

-- Key-value: tek round trip, tek hash probe
GET session:abc123

Yukarıdaki sorgu nedenini gösteriyor: üç join’li index’e karşı tek key. Bu fark düşük trafikte görünmez, yük altında tail latency’ye hâkim olur; session ve sepet verisinin genellikle ilk taşınan şeyler olmasının nedeni budur.

Önemli Performans Özellikleri#

Teknoloji kararları için büyüklük mertebesi değerleri; bunları alıntılanacak ölçüm olarak değil, kendi benchmark’ınızın başlangıç noktası olarak alın:

TeknolojiTipik latency (P99)Tipik throughputMemory profiliEn İyi Kullanım Alanı
Redis<5ms200K+ ops/secKüçük value’larda kompaktCaching, session’lar
DynamoDB10-20ms40K WCU/secManaged overheadServerless app’ler
etcd<25ms30K+ ops/sec8GB limitConfig management
Hazelcast3-30msLinear scalingJVM heap sınırlıJava ekosistemler
Memcached<5ms1M+ ops/secSadece memoryPure caching
IMemoryCache<1msIn-process hızProcess memorySingle server

Relational Database’lere Karşı Temel Avantajlar#

1. O(1) vs O(log n) Erişim Zamanları Direkt hash table lookup’lar vs karmaşık query planning ve execution.

2. Horizontal Scaling Key-value store’lar distributed hash table’lar için tasarlanmış, relational database’ler genellikle vertical scale olur.

3. Schema Esnekliği Veri yapınız evrimleştiğinde migration gerekmez:

// Zaman içinde migration olmadan gelişim
// Versiyon 1
const userSession_v1 = {
  userId: "1001",
  expiresAt: 1642248600
};

// Versiyon 2 (6 ay sonra)
const userSession_v2 = {
  userId: "1001",
  expiresAt: 1642248600,
  preferences: { theme: "dark", language: "tr" },
  deviceInfo: { browser: "Chrome", os: "macOS" }
};

// Versiyon 3 (1 yıl sonra)
const userSession_v3 = {
  userId: "1001",
  expiresAt: 1642248600,
  preferences: { theme: "dark", language: "tr" },
  deviceInfo: { browser: "Chrome", os: "macOS" },
  features: ["beta_feature_1", "experimental_ui"],
  analytics: { lastPageView: "/dashboard", sessionStart: 1642245000 }
};
// Schema migration gerekmez!

Yaklaşım Seçim Kriterleri#

Key-Value Seç:

  • Basit erişim pattern’leri (key ile lookup)
  • Yüksek performance gereksinimleri (<10ms)
  • Esnek schema gereksinimleri
  • Horizontal scaling gerekli
  • Caching veya session management

Relational Seç:

  • Karmaşık JOIN’li sorgular
  • Birden fazla entity’de ACID transaction’lar
  • Reporting ve analytics workload’ları
  • Data integrity constraint’leri kritik

Tech Stack’e Göre Çözümler#

Yaygın teknoloji stack’lerinde KV storage implement etmek için ekosistem bazlı rehber:

Java Ekosistemi#

// Java: Hazelcast embedded örneği
@Service
public class UserSessionService {
    private final IMap<String, UserSession> sessions;

    public UserSessionService() {
        HazelcastInstance hz = Hazelcast.newHazelcastInstance();
        this.sessions = hz.getMap("user-sessions");
    }

    public UserSession getSession(String sessionId) {
        return sessions.get(sessionId);  // Distributed, in-memory
    }
}
ÇözümEntegrasyonEn İyi KullanımEntegrasyon Karmaşıklığı
HazelcastNative JVM embeddingDistributed caching, computationDüşük (native)
RedisJedis, Lettuce client’larExternal caching, session’larOrta
Chronicle MapOff-heap storageLow-latency, büyük dataset’lerYüksek
InfinispanRed Hat ekosistemiJBoss/WildFly entegrasyonuOrta
EhcacheHibernate entegrasyonuJPA second-level cacheDüşük

.NET Ekosistemi#

// .NET: Multi-tier caching örneği
public class CacheService
{
    private readonly IMemoryCache _memoryCache;
    private readonly IDistributedCache _distributedCache;

    public async Task<T> GetAsync<T>(string key)
    {
        // L1: In-memory cache
        if (_memoryCache.TryGetValue(key, out T value))
            return value;

        // L2: Distributed cache (Redis)
        var serialized = await _distributedCache.GetStringAsync(key);
        if (serialized != null)
        {
            value = JsonSerializer.Deserialize<T>(serialized);
            _memoryCache.Set(key, value, TimeSpan.FromMinutes(5));
            return value;
        }

        return default(T);
    }
}
ÇözümEntegrasyonEn İyi KullanımSetup Süresi
IMemoryCacheBuilt-in ASP.NET CoreSingle-server caching1 saat
IDistributedCacheRedis, SQL ServerMulti-server caching1 gün
RedisStackExchange.RedisHigh-performance distributed1 gün
Azure Cache for RedisManaged RedisAzure-native uygulamalar4 saat
SQL Server CacheBuilt-in providerMevcut SQL altyapısı4 saat

Node.js/JavaScript Ekosistemi#

// Node.js: Fallback pattern'li Redis
class CacheService {
    constructor() {
        this.redis = new Redis({
            host: 'localhost',
            port: 6379,
            retryDelayOnFailover: 100,
            maxRetriesPerRequest: 3
        });
        this.memoryCache = new Map();
    }

    async get(key) {
        // L1: In-memory
        if (this.memoryCache.has(key)) {
            return this.memoryCache.get(key);
        }

        // L2: Redis
        try {
            const value = await this.redis.get(key);
            if (value) {
                const parsed = JSON.parse(value);
                this.memoryCache.set(key, parsed);
                setTimeout(() => this.memoryCache.delete(key), 60000); // 1 dakika L1 TTL
                return parsed;
            }
        } catch (error) {
            console.error('Redis error:', error);
        }

        return null;
    }
}

Programming Language Karar Matrisi#

Evet

Hayır

Java

.NET

Node.js

Python

Go

Spring/Hibernate

Genel

Red Hat/JBoss

Configuration

Caching

Local Storage

Key-Value Storage Gerekiyor?

Single Server?

In-Memory Cache

Programming Language?

.NET: IMemoryCache

Node.js: Map/node-cache

Python: dict/cachetools

Go: sync.Map

Ekosistem?

Redis + IDistributedCache

Redis + ioredis

Redis + redis-py

Kullanım Alanı?

Hazelcast/Ehcache

Redis

Infinispan

etcd

Redis

BadgerDB

Pratik Karar Matrisleri#

Bu matrisler teknoloji seçimi kararlarına yardımcı olur:

Kullanım Alanı Bazlı Seçim Matrisi#

Kullanım AlanıBirincil SeçimAlternatifKaçınNedeni
Session Storage (Web App’ler)Redis, IMemoryCache (.NET)DynamoDB (serverless)etcdSession’lar hızlı read/write, TTL desteği gerektirir
Database Query CachingRedis, MemcachedIn-memory (.NET/Java)DynamoDBHızlı eviction policy’ler, maliyet kontrolü gerekli
Configuration Managementetcd, ConsulRedisDynamoDBConsistency, watching, hiyerarşik key’ler gerekli
Real-time AnalyticsRedis (sorted sets)HazelcastMemcachedAtomic işlemler, veri yapıları gerekli
Microservice Communicationetcd, ConsulRedis pub/subFile-basedService discovery, health check’ler gerekli

Mimari Ölçek Karar Matrisi#

ÖlçekSingle ServerMulti-ServerGlobal ScaleCloud-Native
<1K kullanıcıIn-memory cacheIn-memory cacheRedisRedis
1K-10K kullanıcıRedis/IMemoryCacheRedisRedis ClusterDynamoDB/Redis
10K-100K kullanıcıRedisRedis ClusterDynamoDBDynamoDB
100K+ kullanıcıRedis ClusterDynamoDBDynamoDB/Cosmos DBDynamoDB

Teknoloji Seçimi Karar Mantığı#

Evet

.NET

Diger

Hayır

Configuration

Diger

Serverless

Diger

Java + Embedded

Diger

Düşük budget + Ops team yok

Diger

Başlangıç: KV Storage Seçimi

Single Server?

Dil?

IMemoryCache

In-memory cache

Kullanım Alanı?

etcd

Workload Tipi?

DynamoDB

Ekosistem?

Hazelcast

Budget & Ops?

Managed Redis

Redis - Default Seçim

Ekosistemin Kendi Çözümü Atlanıyor#

Distributed caching denince refleks cevap Redis oluyor; cluster farkındalığı olan bir cache ile zaten gelen stack’lerde bile. Redis ekleyen bir Spring Boot servisi ayrı bir process, her lookup’ta bir network hop ve nöbet listesinde bir bileşen daha üstlenir. Aynı JVM’e gömülen Hazelcast, cluster’dan daha uzun yaşaması gerekmeyen cache verisi için bu üçünü de ortadan kaldırır.

İkinci bir runtime aynı veriye ihtiyaç duyduğu anda ya da working set application heap’inde tutmak isteyeceğiniz boyutu aştığında denge tersine döner. O noktada network hop size bağımsızlık ve ayrı boyutlandırabileceğiniz bir bellek bütçesi kazandırır.

Maliyet Değerlendirmeleri ve Trade-off’lar#

100GB civarı bir working set’te faturanın şekli, etiket fiyatından daha belirleyicidir. Aşağıdaki sıralama sağlayıcılar arasında değişmez; mutlak rakamlar bölgeye, instance ailesine ve trafik profiline göre oynar. Karar öncesinde kısa listeye kalan iki üç seçeneği sağlayıcının hesaplayıcısında fiyatlandırın.

ÇözümMaliyet yapısıPerformanceOperasyonel OverheadEn İyi Kullanım
IMemoryCacheEk harcama yok, in-processEn hızlıHiçSingle server
Redis (Self-managed)Sadece instance maliyetiHızlıYüksekMaliyet-hassas
Redis (Managed)Instance maliyeti artı servis primiHızlıDüşükCloud-native uygulamalar
DynamoDBİstek başına veya provisioned kapasiteİyiHiçVariable workload’lar
Cosmos DBProvisioned RU/s artı storageİyiHiçEnterprise
etcdMevcut K8s control plane’de ek harcama yokOrtaOrtaSadece configuration

Bu Seçimlerin Kırılma Noktaları#

IMemoryCache Load Balancer’ın Arkasında Çalışmaz#

IMemoryCache tek bir process’in içinde yaşar. Development ortamında ve tek sunucuda sorunsuz çalışır; load balancer bir sonraki isteği başka bir instance’a yönlendirdiği anda session bulunamaz ve kullanıcı logout olur. Hata aralıklı ve trafiğe bağlı çıktığı için teşhisi pahalıdır.

Tek process’ten daha uzun yaşaması gereken session state’in yeri, Redis veya dengi bir backend’le çalışan IDistributedCache’tir. IMemoryCache’i her instance’ta yeniden üretilmesi ucuz olan veriler için saklayın: parse edilmiş konfigürasyon, lookup tabloları gibi.

Redis-Spesifik Tuzaklar#

# Problem: Redis'te blocking işlemler
SLOW LOG GET 10  # Yavaş işlemleri kontrol et
# Yaygın blocker'lar: KEYS *, FLUSHALL, büyük SORT işlemleri

# Çözüm: Non-blocking alternatifleri kullan
SCAN 0 MATCH "user:*" COUNT 100  # KEYS user:* yerine

DynamoDB Hot Partition Problemi#

// Problem: Kötü partition key dağılımı
const badPartitionKey = `user_${userId}`;  // Tüm user verisi bir partition'da

// Çözüm: Randomization ekle
const goodPartitionKey = `user_${userId}_${timestamp % 10}`;

Sonradan Değiştirmesi Pahalı Kararlar#

Bu kararlar erken alındığında ucuz, sonradan değiştirildiğinde pahalıdır:

  1. Observability ile başla: hit rate, eviction rate, latency ve maliyet, cache production trafiği taşımadan önce görünür olmalı
  2. Bölge kararını erken ver: tek bölgeli bir store gayet makul bir cevaptır, ama tek bölge varsayan bir key şemasına sonradan replikasyon eklemek değildir
  3. Provisioning’i kodda tut: cache cluster’ları yeniden boyutlandırılır, failover yapar ve baştan kurulur; instance elle açıldıysa bunların her biri manuel bir olaya dönüşür

Varsayılan Nerede Geçerli#

Varsayılanı dört durumda geçin. Failover planı olmayan tek sunucuda in-process cache daha yerinde olur. Trafiği dalgalanan serverless workload’lar DynamoDB’nin managed scaling’ine ve istek başına faturalamasına oturur. Configuration ve coordination, ham throughput yerine tutarlılık ve watch için tasarlanmış etcd’ye aittir. Tamamen JVM üzerinde koşan bir deployment Hazelcast’ı gömüp network hop’u atlayabilir.

Hangisini seçerseniz seçin, erişilemez olacağını varsayarak plan yapın: retry’lar, bir circuit breaker ve cache ayakta değilken gelen isteklere karşı tanımlı bir davranış.

Kaynaklar#

İlgili yazılar