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$lshzxekombinasyonu, 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.