Redis Keyspace Notifications Kullanımı ve Yapılandırması

Redis’i sadece basit bir key-value store olarak kullanıyorsanız, aslında onun en güçlü özelliklerinden birini masada bırakıyorsunuz demektir. Keyspace Notifications, Redis içindeki değişiklikleri gerçek zamanlı olarak takip etmenizi sağlayan bir mekanizma. “Şu key expire olduğunda bana haber ver”, “bu hash’e yeni bir alan eklendiğinde tetikle”, “şu listeye eleman push edildiğinde aksiyon al” gibi senaryoları bu özellik sayesinde kolayca hayata geçirebilirsiniz.

Keyspace Notifications Nedir ve Nasıl Çalışır

Redis Keyspace Notifications, 2.8.0 sürümüyle birlikte gelen ve Redis’in pub/sub altyapısı üzerine inşa edilmiş bir olay bildirimi sistemidir. Temel mantık şu: Redis, dahili olarak yaptığı işlemleri (set, del, expire, lpush, vb.) özel kanallara yayınlar, siz de bu kanallara abone olarak ilgili olayları dinlersiniz.

İki farklı kanal tipi vardır:

  • Keyspace kanalları: Belirli bir key üzerinde hangi olayların gerçekleştiğini bildirir. Format: __keyspace@__:
  • Keyevent kanalları: Belirli bir olay tipinin hangi key’ler üzerinde gerçekleştiğini bildirir. Format: __keyevent@__:

Yani __keyspace@0__:mykey kanalına abone olursanız, mykey üzerinde yapılan her işlemi (set, del, expire, vb.) alırsınız. __keyevent@0__:expired kanalına abone olursanız ise hangi key expire olursa olsun haber alırsınız.

Konfigürasyon

Keyspace Notifications varsayılan olarak kapalıdır. Bunun sebebi performans: Her işlem için bildirim üretmek ekstra CPU ve I/O yükü getirir. Bu yüzden sadece gerçekten ihtiyaç duyduğunuz olayları açmanız önerilir.

Konfigürasyon notify-keyspace-events parametresiyle yapılır. Bu parametre bir veya daha fazla karakter kombinasyonunu kabul eder:

  • K: Keyspace olayları, __keyspace@__ prefix’i ile
  • E: Keyevent olayları, __keyevent@__ prefix’i ile
  • g: Genel komutlar (DEL, EXPIRE, RENAME, vb.)
  • $: String komutları (SET, GETSET, vb.)
  • l: List komutları (LPUSH, RPUSH, vb.)
  • s: Set komutları
  • h: Hash komutları
  • z: Sorted set komutları
  • x: Sadece expire olmuş key’ler
  • d: Stream komutları
  • t: Stream komutları (XADD, vb.)
  • e: Evicted key’ler (maxmemory politikası nedeniyle silinen)
  • A: g$lshzxe kombinasyonu, yani tüm olaylar

K veya E harflerinden en az birini kullanmak zorundasınız, yoksa hiçbir bildirim gelmez.

redis.conf Üzerinden Konfigürasyon

# redis.conf dosyasını açın
sudo nano /etc/redis/redis.conf

# Sadece expired olaylarını etkinleştirmek için (en yaygın kullanım)
notify-keyspace-events "Ex"

# Tüm keyevent bildirimlerini etkinleştirmek için
notify-keyspace-events "AE"

# Hem keyspace hem keyevent, sadece expired ve generic olaylar
notify-keyspace-events "KEg$x"

Çalışan Redis’te Runtime Konfigürasyon

Sunucuyu yeniden başlatmadan da ayarı değiştirebilirsiniz:

# Redis CLI ile bağlanın
redis-cli

# Mevcut ayarı kontrol edin
CONFIG GET notify-keyspace-events

# Expired olaylarını etkinleştirin
CONFIG SET notify-keyspace-events "Ex"

# Tüm keyevent bildirimlerini açın
CONFIG SET notify-keyspace-events "KEA"

# Ayarı doğrulayın
CONFIG GET notify-keyspace-events

İlk Test: Expired Key Bildirimlerini Dinlemek

En yaygın kullanım senaryosu TTL süresi dolan key’leri yakalamaktır. Önce bir terminal açıp dinleyicimizi kuralım:

# Terminal 1: Expired event'lerini dinle
redis-cli subscribe __keyevent@0__:expired

Şimdi başka bir terminalde test key’lerimizi oluşturalım:

# Terminal 2: Test key'leri oluştur
redis-cli SET session:user:1001 "active" EX 5
redis-cli SET cache:product:42 "cached_data" EX 3
redis-cli SET temp:token:xyz "abc123" EX 2

Terminal 1’de birkaç saniye içinde şuna benzer çıktılar görmeye başlarsınız:

1) "message"
2) "__keyevent@0__:expired"
3) "temp:token:xyz"

1) "message"
2) "__keyevent@0__:expired"
3) "cache:product:42"

Önemli bir detay: Redis, key’i silerken değil, key’e bir sonraki erişim yapıldığında ya da periyodik temizlik sırasında expired bildirimini gönderir. Gerçek zamanlı değil, “yaklaşık zamanlı” diyebiliriz.

Gerçek Dünya Senaryosu 1: Oturum Süresi Dolduğunda Temizlik

E-ticaret platformlarında kullanıcı sepetlerini Redis’te tutmak yaygın bir pratiktir. Kullanıcı oturumu kapandığında ya da timeout olduğunda sepet verilerini temizlemek istersiniz. İşte bunu Python ile nasıl yaparsınız:

import redis
import json
import logging
from threading import Thread

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class SessionExpiryHandler:
    def __init__(self, redis_host='localhost', redis_port=6379, db=0):
        # Pub/Sub için ayrı bir bağlantı kullanmak best practice
        self.subscriber = redis.Redis(
            host=redis_host,
            port=redis_port,
            db=db,
            decode_responses=True
        )
        self.worker = redis.Redis(
            host=redis_host,
            port=redis_port,
            db=db,
            decode_responses=True
        )

    def cleanup_user_data(self, session_key):
        """Session expire olduğunda ilişkili verileri temizle"""
        # session:user:1001 formatından user_id'yi çıkar
        if not session_key.startswith('session:user:'):
            return

        user_id = session_key.split(':')[-1]
        logger.info(f"Session expired for user: {user_id}")

        # Pipeline ile atomik temizlik
        pipe = self.worker.pipeline()
        pipe.delete(f'cart:{user_id}')
        pipe.delete(f'wishlist:temp:{user_id}')
        pipe.delete(f'checkout:draft:{user_id}')
        pipe.srem('active_users', user_id)
        results = pipe.execute()

        logger.info(f"Cleaned up data for user {user_id}: {results}")

        # Audit log veya analytics için event gönder
        audit_event = {
            'event': 'session_expired',
            'user_id': user_id,
            'timestamp': self.worker.time()[0]
        }
        self.worker.lpush('audit:session_events', json.dumps(audit_event))
        self.worker.ltrim('audit:session_events', 0, 9999)  # Son 10k eventi tut

    def listen(self):
        pubsub = self.subscriber.pubsub()
        pubsub.subscribe('__keyevent@0__:expired')

        logger.info("Listening for expired session events...")

        for message in pubsub.listen():
            if message['type'] == 'message':
                expired_key = message['data']
                try:
                    self.cleanup_user_data(expired_key)
                except Exception as e:
                    logger.error(f"Error handling expiry for {expired_key}: {e}")

    def start(self):
        thread = Thread(target=self.listen, daemon=True)
        thread.start()
        return thread

if __name__ == '__main__':
    handler = SessionExpiryHandler()
    thread = handler.start()
    thread.join()

Gerçek Dünya Senaryosu 2: Distributed Lock ile Deadlock Tespiti

Mikroservis mimarilerinde distributed lock kullanıyorsanız, bir servis çöktüğünde kilidi alan işlem sonlanmaz ve deadlock oluşabilir. Keyspace Notifications ile bu durumu tespit edebilirsiniz:

# Önce konfigürasyonu ayarlayalım
redis-cli CONFIG SET notify-keyspace-events "KExg"

# Lock key'lerini izlemek için pattern subscription
redis-cli psubscribe "__keyevent@0__:expired" "__keyevent@0__:del"
import redis
import time
import uuid

class DistributedLockMonitor:
    def __init__(self):
        self.r = redis.Redis(decode_responses=True)
        self.locks_registry = {}  # lock_key -> {owner, acquired_at, expected_ttl}

    def acquire_lock(self, resource, owner, ttl=30):
        lock_key = f"lock:{resource}"
        lock_id = str(uuid.uuid4())
        value = f"{owner}:{lock_id}"

        # SET NX ile atomik lock alma
        acquired = self.r.set(lock_key, value, nx=True, ex=ttl)

        if acquired:
            # Registry'e kaydet
            self.r.hset(f"lock_registry:{lock_key}", mapping={
                'owner': owner,
                'lock_id': lock_id,
                'acquired_at': int(time.time()),
                'ttl': ttl,
                'resource': resource
            })
            self.r.expire(f"lock_registry:{lock_key}", ttl + 60)
            return lock_id

        return None

    def monitor_lock_expiries(self):
        pubsub = self.r.pubsub()
        pubsub.psubscribe('__keyevent@0__:expired')

        for message in pubsub.listen():
            if message['type'] == 'pmessage':
                expired_key = message['data']

                if expired_key.startswith('lock:'):
                    resource = expired_key[5:]  # "lock:" prefix'ini çıkar
                    registry_key = f"lock_registry:{expired_key}"
                    registry_data = self.r.hgetall(registry_key)

                    if registry_data:
                        print(f"UYARI: Lock süresi doldu!")
                        print(f"  Resource: {resource}")
                        print(f"  Owner: {registry_data.get('owner')}")
                        print(f"  TTL: {registry_data.get('ttl')} saniye")

                        # Alert sisteminize gönderin
                        self.r.lpush('alerts:lock_timeouts', str({
                            'resource': resource,
                            'owner': registry_data.get('owner'),
                            'timestamp': int(time.time())
                        }))

Gerçek Dünya Senaryosu 3: Cache Invalidation Zinciri

Büyük uygulamalarda bir verinin birden fazla cache kaydını invalidate etmeniz gerekebilir. Örneğin bir ürün güncellendiğinde ürün sayfası cache’i, kategori listesi cache’i ve arama sonuçları cache’i temizlenmelidir:

# Keyspace ve keyevent bildirimlerini aç
redis-cli CONFIG SET notify-keyspace-events "KEg$"

# Belirli bir key'i izle
redis-cli subscribe "__keyspace@0__:product:42"
import redis
import re

class CacheInvalidationChain:
    def __init__(self):
        self.subscriber = redis.Redis(decode_responses=True)
        self.cache = redis.Redis(decode_responses=True)

        # Hangi key değiştiğinde hangi pattern'lerin silineceğini tanımla
        self.invalidation_rules = {
            r'^product:(d+)$': [
                'cache:product_page:{id}',
                'cache:product_detail:{id}',
                'cache:related_products:{id}',
                'search:product_index'
            ],
            r'^category:(d+)$': [
                'cache:category_page:{id}',
                'cache:category_products:{id}',
                'cache:nav_menu'
            ],
            r'^user:(d+)$': [
                'cache:user_profile:{id}',
                'cache:user_orders:{id}'
            ]
        }

    def process_change(self, changed_key, event_type):
        """Değişen key için invalidation zincirini çalıştır"""
        if event_type not in ('set', 'hset', 'del', 'hdel'):
            return

        for pattern, dependent_keys in self.invalidation_rules.items():
            match = re.match(pattern, changed_key)
            if match:
                entity_id = match.group(1)
                keys_to_delete = [k.format(id=entity_id) for k in dependent_keys]

                # UNLINK ile non-blocking silme (DEL'den daha iyi)
                deleted = self.cache.unlink(*keys_to_delete)
                print(f"Cache invalidation: {changed_key} degisti, "
                      f"{deleted} cache key silindi")
                break

    def start_listening(self, db=0):
        pubsub = self.subscriber.pubsub()

        # Tüm keyspace olaylarını izle
        pubsub.psubscribe(f'__keyspace@{db}__:*')

        print(f"Cache invalidation listener baslatildi (DB: {db})")

        for message in pubsub.listen():
            if message['type'] == 'pmessage':
                # Kanal adından key'i çıkar
                channel = message['channel']
                key = channel.split(f'__keyspace@{db}__:')[1]
                event = message['data']

                self.process_change(key, event)

Node.js ile Kullanım

Eğer Node.js stack’iniz varsa, ioredis kütüphanesi ile keyspace notifications şöyle kullanılır:

const Redis = require('ioredis');

// Pub/Sub için ayrı bağlantı açmak şart
const subscriber = new Redis({
  host: 'localhost',
  port: 6379,
  db: 0
});

const client = new Redis({
  host: 'localhost',
  port: 6379,
  db: 0
});

// Konfigürasyonu programatik olarak set et
async function setupNotifications() {
  await client.config('SET', 'notify-keyspace-events', 'Ex');
  console.log('Keyspace notifications aktif edildi');
}

// Pattern ile abone ol
async function startListening() {
  await setupNotifications();

  // Tüm expired event'lerini dinle
  await subscriber.psubscribe('__keyevent@0__:expired');

  subscriber.on('pmessage', async (pattern, channel, expiredKey) => {
    console.log(`Key expired: ${expiredKey}`);

    // Rate limiting için kullanım örneği
    if (expiredKey.startsWith('ratelimit:')) {
      const userId = expiredKey.split(':')[1];
      console.log(`Rate limit penceresi kapandi, kullanici serbest: ${userId}`);

      // Kullanıcının request counter'ını sıfırla
      await client.del(`request_count:${userId}`);
    }

    // OTP token süresi dolduğunda audit log
    if (expiredKey.startsWith('otp:')) {
      const otpData = expiredKey.split(':');
      const userId = otpData[1];

      await client.lpush('security:otp_expired', JSON.stringify({
        userId,
        timestamp: Date.now(),
        key: expiredKey
      }));
    }
  });

  console.log('Expired event listener aktif');
}

startListening().catch(console.error);

Performans ve Dikkat Edilmesi Gerekenler

Keyspace Notifications kullanırken göz önünde bulundurmanız gereken bazı kritik noktalar var.

Pub/Sub Bağlantılarını Yönetin

# Kaç adet pub/sub client bağlı olduğunu kontrol edin
redis-cli CLIENT LIST | grep -c "flags=S"

# Info ile pub/sub istatistiklerini görün
redis-cli INFO stats | grep pubsub

Pub/Sub bağlantıları Redis’te sürekli açık kalır ve her biri bellek tüketir. Uygulama kodunuzda bağlantı havuzu kullanıyorsanız, pub/sub için bu havuzdan bağlantı ALMAYINIZ. Her pub/sub listener için dedicated, ayrı bir bağlantı açın.

Büyük Veri Tabanlarında Dikkat

Milyonlarca key içeren bir Redis instance’ında notify-keyspace-events "KA" gibi bir konfigürasyon felaket tarifi olabilir. Sadece ihtiyacınız olan olay tiplerini açın:

# Kotu: Her seyi dinle
redis-cli CONFIG SET notify-keyspace-events "KA"

# İyi: Sadece ihtiyaciniz olan event'leri dinle
redis-cli CONFIG SET notify-keyspace-events "Ex"  # Sadece expired
redis-cli CONFIG SET notify-keyspace-events "Eg"  # Expired + generic (del, rename)

Expired Olaylarının Gecikmesi

Redis’in lazy expiration mekanizması nedeniyle bir key TTL’si dolduğunda hemen silinmez. Silinmesi ya o key’e erişildiğinde ya da periyodik background taraması sırasında gerçekleşir. Bu nedenle expired bildirimlerinde birkaç saniyelik gecikme olabilir. Eğer milisaniye hassasiyeti gerektiren bir sistemdeyseniz bu yaklaşım uygun olmayabilir.

Redis Cluster’da Kullanım

Redis Cluster kullanıyorsanız, her node kendi key’leri için bildirim üretir. Tüm cluster’ı izlemek için her node’a ayrı ayrı subscribe olmanız gerekir:

# Her node için ayrı subscribe
redis-cli -h node1 -p 7000 subscribe __keyevent@0__:expired
redis-cli -h node2 -p 7001 subscribe __keyevent@0__:expired
redis-cli -h node3 -p 7002 subscribe __keyevent@0__:expired

Monitoring: Notification Sağlığını İzlemek

# Kaç mesaj publish edildiğini izle
redis-cli INFO stats | grep -E "pubsub_channels|pubsub_patterns|total_commands_processed"

# Gerçek zamanlı monitoring
redis-cli --stat

# Debug: Hangi event'lerin geldiğini logla
redis-cli monitor | grep -E "PUBLISH|subscribe"
# Keyspace notification test scripti
#!/bin/bash

echo "Keyspace notification testi basliyor..."

# Bildirimleri etkinlestir
redis-cli CONFIG SET notify-keyspace-events "Ex" > /dev/null

# Background'da dinleyici baslat
redis-cli subscribe __keyevent@0__:expired &
LISTENER_PID=$!

sleep 1

# Test key'leri olustur
redis-cli SET test:notification:1 "value1" EX 2 > /dev/null
redis-cli SET test:notification:2 "value2" EX 3 > /dev/null

echo "Test key'leri olusturuldu, expire bekleniyor..."
sleep 5

# Dinleyiciyi kapat
kill $LISTENER_PID 2>/dev/null

echo "Test tamamlandi"

Sonuç

Redis Keyspace Notifications, doğru kullanıldığında uygulamanıza reaktif bir katman ekler. Session cleanup, cache invalidation, distributed lock monitoring, rate limiting pencerelerinin yönetimi gibi onlarca farklı senaryoda bu özellikten faydalanabilirsiniz.

En kritik nokta şu: İhtiyacınız olmayan olayları asla etkinleştirmeyin. notify-keyspace-events "Ex" ile sadece expired olaylarını dinlemek, notify-keyspace-events "KA" ile her şeyi dinlemekten çok farklı bir performans profili çizer. Redis’in pub/sub altyapısı sağlam ama sınırsız değil.

Üretim ortamında kullanmadan önce yük testinizi yapın, subscriber sayınızı ve mesaj hacminizi ölçün. Dedicated pub/sub bağlantıları kullanın, connection pool’u pub/sub için karıştırmayın. Ve her zaman fallback mekanizmanızı hazırlayın çünkü özellikle expired olaylarında gecikme yaşanabilir.

Bu özelliği bir kez doğru kurduğunuzda, polling-based çözümlerin yerini event-driven bir mimariye bıraktığını ve hem kodunuzun hem de Redis yükünüzün ne kadar sadeleştiğini göreceksiniz.

Yorum yapın