Redis Önbellekleme Mimarisi: Temel Kararlar

## Redis Önbellekleme Mimarisi: Temel Kararlar Önbellek, veritabanı yükünü azaltmak ve yanıt süresini düşürmek için kullanılır; ancak yanlış kurgulan...

Redis Önbellekleme Mimarisi: Temel Kararlar

Redis Önbellekleme Mimarisi: Temel Kararlar

Önbellek, veritabanı yükünü azaltmak ve yanıt süresini düşürmek için kullanılır; ancak yanlış kurgulandığında tutarlılık sorunları ve ani yük dalgalanmaları üretir. Aşağıdaki adımlar, orta seviye bir geliştiricinin Redis'i önbellek katmanı olarak kurarken vermesi gereken kararları sıralar. Örnekler Redis 7.x davranışına göre yazılmıştır; komutlar Redis 6 ve sonrasında da geçerlidir.

Önkoşullar

  • Çalışan bir Redis sunucusu (yerel veya yönetilen hizmet).
  • Uygulama tarafında bir Redis istemcisi. Örneklerde redis-py (Python) kullanılıyor; komutlar dil bağımsızdır.
  • Önbelleğe alınacak verinin kaynağı olan bir veritabanı veya harici servis.

1. Önbellek Stratejisini Seç

İki yaygın yaklaşım vardır:

  • Cache-aside (lazy loading): Uygulama önce Redis'e bakar; yoksa kaynaktan okur, Redis'e yazar. En yaygın ve en esnek yöntemdir.
  • Write-through: Yazma işlemi hem kaynağa hem önbelleğe yapılır. Tutarlılık daha kolay korunur, ancak yazma maliyeti artar.

Çoğu senaryoda cache-aside ile başlamak yeterlidir. Aşağıdaki akış bu stratejiyi izler.

2. Anahtar (Key) Tasarımı

Anahtarlar öngörülebilir, çakışmayan ve versiyonlanabilir olmalıdır.

kullanici:{id}:profil
urun:{id}:detay:v2
  • {id} gibi yer tutucuları gerçek değerle değiştir.
  • Şema değiştiğinde eski anahtarları geçersiz kılmak için sona sürüm ekle (:v2).
  • Anahtarları çok uzun tutma; bellek ve okunabilirlik açısından kısa ama ayırt edici ol.

3. TTL Belirle

Her önbellek girdisine son kullanma süresi ver. Süresiz anahtarlar zamanla belleği doldurur ve bayat veri riskini artırır.

import redis
import json

r = redis.Redis(host="localhost", port=6379, decode_responses=True)

def profil_getir(kullanici_id):
    anahtar = f"kullanici:{kullanici_id}:profil"
    veri = r.get(anahtar)
    if veri is not None:
        return json.loads(veri)

    profil = veritabani_profil_oku(kullanici_id)  # kaynak okuma
    r.set(anahtar, json.dumps(profil), ex=300)     # 5 dakika TTL
    return profil

Beklenen davranış: İlk çağrıda kaynaktan okunur ve Redis'e yazılır; sonraki çağrılar TTL dolana kadar Redis'ten döner. TTL dolduğunda anahtar silinir ve akış baştan işler.

TTL seçimi verinin değişme sıklığına bağlıdır. Sık değişen veride kısa, nadiren değişen veride uzun TTL mantıklıdır. Kesin bir sayı vermek yerine, kabul edilebilir bayatlık süresini ölçüp ona göre belirle.

4. Veri Değişince Önbelleği Geçersiz Kıl

Kaynak veri güncellendiğinde ilgili anahtarı sil veya güncelle. Silmek genelde daha güvenlidir; çünkü bir sonraki okuma güncel veriyi yeniden yükler.

def profil_guncelle(kullanici_id, yeni_profil):
    veritabani_profil_yaz(kullanici_id, yeni_profil)
    r.delete(f"kullanici:{kullanici_id}:profil")

Burada dikkat edilecek nokta sıralamadır: önce kaynağa yaz, sonra önbelleği sil. Tersi yapılırsa, arada gelen bir okuma eski veriyi yeniden önbelleğe alabilir.

Cache Stampede Nedir?

Popüler bir anahtarın TTL'i dolduğunda, o anda gelen çok sayıda istek aynı anda kaynağa yönelir. Buna cache stampede (veya thundering herd) denir. Etkisi, tekil bir anahtarın süresi dolduğunda veritabanında ani ve yüksek bir yük oluşmasıdır.

Bu bölümdeki çözümler birbirini dışlamaz; birlikte kullanılabilir.

Çözüm 1: Kilit ile Tek Yükleyici (Lock / Mutex)

Yalnızca bir istek kaynaktan okur, diğerleri kısa süre bekler. Redis'te SET komutunun NX ve PX seçenekleri dağıtık kilit için kullanılabilir.

import time
import uuid

def profil_getir_kilitli(kullanici_id, deneme=5):
    anahtar = f"kullanici:{kullanici_id}:profil"
    veri = r.get(anahtar)
    if veri is not None:
        return json.loads(veri)

    kilit = f"kilit:{anahtar}"
    kilit_degeri = str(uuid.uuid4())
    # 10 saniyelik kilit; yalnızca anahtar yoksa kurulur
    alindi = r.set(kilit, kilit_degeri, nx=True, px=10000)

    if alindi:
        try:
            profil = veritabani_profil_oku(kullanici_id)
            r.set(anahtar, json.dumps(profil), ex=300)
            return profil
        finally:
            # Yalnızca kendi kurduğumuz kilidi sil
            if r.get(kilit) == kilit_degeri:
                r.delete(kilit)
    else:
        # Kilit başkasında; kısa bekleyip tekrar dene
        for _ in range(deneme):
            time.sleep(0.05)
            veri = r.get(anahtar)
            if veri is not None:
                return json.loads(veri)
        # Bekleme sonunda hâlâ yoksa kaynaktan oku (fallback)
        return veritabani_profil_oku(kullanici_id)

Beklenen davranış: Eşzamanlı isteklerden yalnızca biri kaynağa gider; diğerleri kısa bekleme sonrası önbellekten okur. Kilit sahibi çökerse px süresi dolunca kilit serbest kalır.

Kilidi silerken değer kontrolü önemlidir. Kontrol olmadan delete çağırmak, süresi dolmuş ve başka bir isteğin yeniden kurduğu kilidi yanlışlıkla silebilir.

Çözüm 2: Olasılıksal Erken Yenileme (Probabilistic Early Expiration)

TTL dolmadan, artan olasılıkla önbelleği yenile. Bu yaklaşım, tüm istemcilerin aynı anda yenilemeye çalışmasını engeller.

import random
import math

def profil_getir_erken(kullanici_id):
    anahtar = f"kullanici:{kullanici_id}:profil"
    veri = r.get(anahtar)
    ttl = r.ttl(anahtar)  # saniye cinsinden kalan süre

    # TTL dolmuş ya da erken yenileme eşiği geçildiyse yenile
    if veri is None or ttl < 0 or random.random() < math.exp(-ttl / 30):
        profil = veritabani_profil_oku(kullanici_id)
        r.set(anahtar, json.dumps(profil), ex=300)
        return profil

    return json.loads(veri)

Buradaki 30 değeri ölçek parametresidir; TTL'e göre ayarla. Beklenen davranış: TTL azaldıkça yenileme olasılığı artar, böylece yük zaman içine yayılır. Kesin bir "doğru" sabit yoktur; kendi trafik profilinle test etmen gerekir.

Çözüm 3: Bayat Veri ile Birlikte Yenileme (Stale-While-Revalidate)

Önbellekteki veri bayatladığında yine de döndürülür, arka planda yenilenir. Kullanıcı beklemez; kısa süreli bayat veri kabul edilebilirse uygundur.

def profil_getir_bayat(kullanici_id):
    anahtar = f"kullanici:{kullanici_id}:profil"
    veri = r.get(anahtar)
    ttl = r.ttl(anahtar)

    if veri is not None:
        # TTL bitmek üzereyse arka planda yenile
        if 0 <= ttl < 30:
            arka_planda_yenile(kullanici_id)
        return json.loads(veri)

    # Hiç veri yoksa senkron oku
    profil = veritabani_profil_oku(kullanici_id)
    r.set(anahtar, json.dumps(profil), ex=300)
    return profil

arka_planda_yenile fonksiyonunun ayrı bir iş parçacığı, kuyruk veya zamanlanmış görevle çalıştırılması gerekir. Beklenen davranış: Kullanıcı her zaman hızlı yanıt alır; yalnızca ilk yükleme senkron olur.

Yaygın Hatalar

  • Önbellek silme sırasını karıştırmak: Önce önbelleği silip sonra kaynağa yazmak, arada gelen okumaların eski veriyi yeniden önbelleklemesine yol açar.
  • TTL'siz anahtar bırakmak: Bellek zamanla dolar; Redis maxmemory politikasına göre beklenmedik anahtarlar tahliye edilir.
  • Kilit süresini çok kısa tutmak: İşlem bitmeden kilit dolarsa aynı anda birden fazla yükleyici kaynağa gider. Süreyi işlem süresine göre ayarla.
  • Kilit silmede değer kontrolü yapmamak: Yukarıda açıklandığı gibi, başkasının kilidini silme riski doğar.
  • Büyük değerleri tek anahtara yığmak: Tek bir GET/SET büyük nesnelerle ağ ve bellek maliyetini artırır; gerekiyorsa veriyi parçala.

Güvenlik Etkileri

  • Erişim kontrolü: Redis'e yalnızca uygulama ağdan erişebilmeli. Redis'i doğrudan internete açmak, kimlik doğrulama yapılandırılmadıysa tüm veriyi açığa çıkarır.
  • Kimlik doğrulama: requirepass veya ACL ile erişimi sınırla. Redis 6 ve sonrasında ACL kullanıcı/izin bazlı erişim sağlar.
  • Şifreleme: Redis trafiği varsayılan olarak şifresizdir; farklı ağlar arasında TLS kullan.
  • Hassas veri: Önbelleğe kimlik bilgisi, token veya kişisel veri yazıyorsan TTL'i kısa tut ve erişimi kısıtla. Önbellek, kalıcı depolama değildir; beklenmedik anda tahliye edilebilir.
  • Komut enjeksiyonu: Anahtarları kullanıcı girdisinden oluşturuyorsan, girdiyi doğrula ve normalize et; beklenmedik anahtar kalıpları üretme.

Sonuç

Önbellek mimarisi üç karara dayanır: hangi strateji (cache-aside çoğu durumda yeterli), anahtar ve TTL tasarımı, ve veri değişiminde geçersiz kılma. Cache stampede ise bu yapının doğal bir yan etkisidir; kilit, olasılıksal erken yenileme veya bayat-veri yaklaşımlarından biriyle yönetilebilir. Hangi çözümün uygun olduğu, verinin bayatlığa ne kadar toleranslı olduğuna ve trafik profilinin ne kadar ani olduğuna bağlıdır. TTL ve kilit süreleri gibi parametreleri kendi ortamında ölçerek belirle.

İLGİLİ İÇERİKLER

Bunları da Okumak İsteyebilirsiniz