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.
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:
| Teknoloji | Tipik latency (P99) | Tipik throughput | Memory profili | En İyi Kullanım Alanı |
|---|---|---|---|---|
| Redis | <5ms | 200K+ ops/sec | Küçük value’larda kompakt | Caching, session’lar |
| DynamoDB | 10-20ms | 40K WCU/sec | Managed overhead | Serverless app’ler |
| etcd | <25ms | 30K+ ops/sec | 8GB limit | Config management |
| Hazelcast | 3-30ms | Linear scaling | JVM heap sınırlı | Java ekosistemler |
| Memcached | <5ms | 1M+ ops/sec | Sadece memory | Pure caching |
| IMemoryCache | <1ms | In-process hız | Process memory | Single 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üm | Entegrasyon | En İyi Kullanım | Entegrasyon Karmaşıklığı |
|---|---|---|---|
| Hazelcast | Native JVM embedding | Distributed caching, computation | Düşük (native) |
| Redis | Jedis, Lettuce client’lar | External caching, session’lar | Orta |
| Chronicle Map | Off-heap storage | Low-latency, büyük dataset’ler | Yüksek |
| Infinispan | Red Hat ekosistemi | JBoss/WildFly entegrasyonu | Orta |
| Ehcache | Hibernate entegrasyonu | JPA second-level cache | Düşü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üm | Entegrasyon | En İyi Kullanım | Setup Süresi |
|---|---|---|---|
| IMemoryCache | Built-in ASP.NET Core | Single-server caching | 1 saat |
| IDistributedCache | Redis, SQL Server | Multi-server caching | 1 gün |
| Redis | StackExchange.Redis | High-performance distributed | 1 gün |
| Azure Cache for Redis | Managed Redis | Azure-native uygulamalar | 4 saat |
| SQL Server Cache | Built-in provider | Mevcut 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#
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çim | Alternatif | Kaçın | Nedeni |
|---|---|---|---|---|
| Session Storage (Web App’ler) | Redis, IMemoryCache (.NET) | DynamoDB (serverless) | etcd | Session’lar hızlı read/write, TTL desteği gerektirir |
| Database Query Caching | Redis, Memcached | In-memory (.NET/Java) | DynamoDB | Hızlı eviction policy’ler, maliyet kontrolü gerekli |
| Configuration Management | etcd, Consul | Redis | DynamoDB | Consistency, watching, hiyerarşik key’ler gerekli |
| Real-time Analytics | Redis (sorted sets) | Hazelcast | Memcached | Atomic işlemler, veri yapıları gerekli |
| Microservice Communication | etcd, Consul | Redis pub/sub | File-based | Service discovery, health check’ler gerekli |
Mimari Ölçek Karar Matrisi#
| Ölçek | Single Server | Multi-Server | Global Scale | Cloud-Native |
|---|---|---|---|---|
| <1K kullanıcı | In-memory cache | In-memory cache | Redis | Redis |
| 1K-10K kullanıcı | Redis/IMemoryCache | Redis | Redis Cluster | DynamoDB/Redis |
| 10K-100K kullanıcı | Redis | Redis Cluster | DynamoDB | DynamoDB |
| 100K+ kullanıcı | Redis Cluster | DynamoDB | DynamoDB/Cosmos DB | DynamoDB |
Teknoloji Seçimi Karar Mantığı#
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üm | Maliyet yapısı | Performance | Operasyonel Overhead | En İyi Kullanım |
|---|---|---|---|---|
| IMemoryCache | Ek harcama yok, in-process | En hızlı | Hiç | Single server |
| Redis (Self-managed) | Sadece instance maliyeti | Hızlı | Yüksek | Maliyet-hassas |
| Redis (Managed) | Instance maliyeti artı servis primi | Hızlı | Düşük | Cloud-native uygulamalar |
| DynamoDB | İstek başına veya provisioned kapasite | İyi | Hiç | Variable workload’lar |
| Cosmos DB | Provisioned RU/s artı storage | İyi | Hiç | Enterprise |
| etcd | Mevcut K8s control plane’de ek harcama yok | Orta | Orta | Sadece 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:
- Observability ile başla: hit rate, eviction rate, latency ve maliyet, cache production trafiği taşımadan önce görünür olmalı
- 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
- 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#
- Redis Dokümantasyonu (yeni sekmede açılır) - Tüm veri türlerini, komutları, yapılandırmayı ve dağıtım kalıplarını kapsayan resmi Redis dokümantasyonu.
- Redis Veri Türleri (yeni sekmede açılır) - Redis veri yapılarına kapsamlı rehber: string, liste, küme, sıralı küme, hash, akış ve daha fazlası.
- Redis Kalıcılık (RDB ve AOF) (yeni sekmede açılır) - Redis kalıcılık seçeneklerini, RDB anlık görüntüleri ile AOF günlük tutma arasındaki değiş tokuşları kapsayan resmi dokümantasyon.
- Redis Replikasyon (yeni sekmede açılır) - Redis lider-takipçi replikasyonuna, yapılandırma, yük devretme ve tutarlılık garantilerine resmi rehber.
- Redis’i Bellek İçi Veri Yapısı Deposu Olarak Kullanma (yeni sekmede açılır) - Redis temel kavramlarını ve yaygın kullanım durumlarını gösteren hızlı başlangıç rehberi.
- Amazon DynamoDB Geliştirici Rehberi (yeni sekmede açılır) - Veri modeli, partition key tasarımı, kapasite modları ve DynamoDB’nin ölçeklenme davranışının arkasındaki kısıtlar.
- etcd Dokümantasyonu (yeni sekmede açılır) - Depolama sınırları, watch semantiği ve konfigürasyon iş yüklerinin dayandığı tutarlılık modeli dahil resmi etcd dokümantasyonu.
- Hazelcast Platform Dokümantasyonu (yeni sekmede açılır) - Gömülü ve client-server dağıtım topolojileri, dağıtık map’ler ve JVM bellek değerlendirmeleri için referans.
- ASP.NET Core’da Distributed Caching (yeni sekmede açılır) - IDistributedCache, Redis ve SQL Server sağlayıcıları ve in-process caching’in nerede yetmediği üzerine Microsoft rehberi.
- Memcached Wiki (yeni sekmede açılır) - Protokolü, slab tabanlı bellek ayrımını ve eviction davranışını kapsayan proje dokümantasyonu.
İlgili yazılar
Çok katmanlı caching için pratik rehber: in-memory, Redis ve CDN katmanları, cache-aside ve write-through, ElastiCache ve MemoryDB, stampede önleme.
caching · redis · aws +4
SQL, NoSQL, NewSQL ve edge seçenekleri arasında doğru veritabanını seçme rehberi: her kategorinin trade-off'ları, seçim kriterleri ve bir karar çerçevesi.
data-storage-orm · postgresql · redis +6
Kimlik okumaları DynamoDB'de kalsın; keyfi sıralama, filtre, facet ve tam metin arama zero-ETL OpenSearch okuma modeline gitsin. Bazen tek PostgreSQL ikisini de yener.
dynamodb · aws · architecture +2
Single Table Design'da DynamoDB throttling'i önleme ve yönetme: partition key tasarımı, write sharding, kapasite modları, DAX caching ve retry pattern'leri.
dynamodb · aws · reliability +4
DynamoDB single-table design'da ilişki modelleme, GSI ve LSI seçimi, DAX optimizasyonu ve production'da yaygın hatalardan kaçınmayı pratik örneklerle öğren.
dynamodb · data-storage-orm · aws +1