Redis Sadece Bir Önbellek Değildir

Redis genellikle "önbellek olarak kullanılan bir bellek içi anahtar/değer deposu" olarak tanımlanır. Bu tanım en yaygın kullanım durumunu kapsar ancak onu tamamen gizler sistemin gerçek gücü. Redis 7.x, 10 tür yerel veri yapısı sunar, her biri belirli erişim kalıpları için optimize edilmiştir; verileri modellemenize gerek yoktur raporlarda veya belgelerde bunları zaten sorgularınızı yansıtan formatta saklarsınız.

Ne Öğreneceksiniz

  • Hash: Bireysel alanlara O(1) erişimi olan yapılandırılmış nesneleri modelleyin
  • Küme ve Sıralanmış Küme: kümeler, kesişimler ve puana göre sıralama
  • ZADD, ZRANK ve ZRANGEBYSCORE ile gerçek zamanlı skor tablosu
  • HyperLogLog: Sabit 12 KB ile yaklaşık kardinalite sayısı
  • Jeo-uzamsal: Yakınlık araması için GEOADD, GEORADIUS ve GEODIST
  • Her bir yapı ne zaman kullanılmalı ve hangisinden kaçınılmalıdır?

Hash: Redis'teki Yapılandırılmış Nesneler

Redis Hash, tek bir anahtarla ilişkili alan değeri çiftlerinin haritasıdır. Bir nesneyi (bir kullanıcı, bir ürün, bir oturum) temsil etmenin doğal yoludur. her şeyi JSON'a serileştirmeden. En önemli avantaj: okuyabilir veya güncelleyebilirsiniz nesnenin tamamını yüklemeden O(1)'de tek bir alan.

# Redis Hash: operazioni base
# HSET key field value [field value ...]
HSET user:1001 name "Mario Rossi" email "mario@example.com" age 35 city "Milano"

# HGET: singolo campo
HGET user:1001 name        # "Mario Rossi"
HGET user:1001 email       # "mario@example.com"

# HMGET: piu campi in una sola round trip
HMGET user:1001 name email city
# 1) "Mario Rossi"
# 2) "mario@example.com"
# 3) "Milano"

# HGETALL: tutto il hash (attenzione su hash grandi!)
HGETALL user:1001
# 1) "name"
# 2) "Mario Rossi"
# 3) "email"
# 4) "mario@example.com"
# 5) "age"
# 6) "35"
# 7) "city"
# 8) "Milano"

# HINCRBY: incremento atomico su campi numerici
HINCRBY user:1001 login_count 1

# HEXISTS: verifica esistenza campo
HEXISTS user:1001 phone     # 0 (non esiste)
HEXISTS user:1001 name      # 1

# HDEL: elimina campi specifici
HDEL user:1001 city

# HLEN: numero di campi
HLEN user:1001              # 4 (age, name, email, login_count)
# Python con redis-py
import redis

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

# Salva un oggetto utente
user_data = {
    'name': 'Mario Rossi',
    'email': 'mario@example.com',
    'age': '35',
    'city': 'Milano',
    'login_count': '0',
}
r.hset('user:1001', mapping=user_data)

# Lettura parziale: solo i campi che servono
name, email = r.hmget('user:1001', ['name', 'email'])
print(f"{name} <{email}>")  # Mario Rossi <mario@example.com>

# Incremento atomico del contatore login
new_count = r.hincrby('user:1001', 'login_count', 1)
print(f"Login count: {new_count}")  # 1

# Pattern: cache object con TTL
r.hset('session:abc123', mapping={
    'user_id': '1001',
    'role': 'admin',
    'created_at': '1710000000',
})
r.expire('session:abc123', 3600)  # 1 ora di TTL

# Lettura selettiva su campo singolo: O(1)
user_id = r.hget('session:abc123', 'user_id')  # '1001'

Setler: Benzersiz Setler ve Setlerdeki İşlemler

Redis Kümesi, benzersiz dizelerin sırasız bir koleksiyonudur. Setlerin gücü kümeler arasındaki işlemlerde yatmaktadır: SUNION, SINTER, SDIFF birleşimi, kesişimi hesaplar ve verileri istemciye getirmek zorunda kalmadan sunucu tarafında farklılık.

# Redis Set: tag system e operazioni su insiemi

# Aggiungi tag a articoli (SADD è idempotente per duplicati)
SADD article:100:tags "python" "fastapi" "backend"
SADD article:200:tags "python" "django" "web"
SADD article:300:tags "rust" "backend" "systems"

# SMEMBERS: tutti i membri (non ordinato)
SMEMBERS article:100:tags
# 1) "python"
# 2) "backend"
# 3) "fastapi"

# SISMEMBER: check membership O(1)
SISMEMBER article:100:tags "python"   # 1
SISMEMBER article:100:tags "java"     # 0

# SCARD: cardinalita del set
SCARD article:100:tags    # 3

# SINTER: articoli con tag in comune (python + backend)
SINTER article:100:tags article:300:tags
# 1) "backend"

# SUNION: tutti i tag di entrambi gli articoli
SUNION article:100:tags article:200:tags
# 1) "python"
# 2) "fastapi"
# 3) "backend"
# 4) "django"
# 5) "web"

# SDIFF: tag in article:100 ma NON in article:200
SDIFF article:100:tags article:200:tags
# 1) "fastapi"
# 2) "backend"

# SMOVE: sposta membro da un set all'altro (atomico)
SMOVE article:100:tags article:300:tags "backend"

# SRANDMEMBER: N elementi casuali (utile per sampling)
SRANDMEMBER article:100:tags 2

Sıralanmış Küme: Skor Tablosu ve Aralık Sorguları

Sıralanmış Küme, Redis'teki en çok yönlü veri yapısıdır. Her element bir tane var Gol ilgili şamandıra; set otomatik olarak sıralanır puana göre. Öğeleri pozisyona (sıraya) veya puan aralığına göre okuyabilirsiniz, hepsi O(log N) cinsinden. Liderlik tabloları, zaman çizelgeleri ve sıralar için doğal seçimdir öncelik ve aralık bazlı filtreleme.

# Sorted Set: leaderboard gaming in tempo reale

# ZADD key score member
ZADD leaderboard:weekly 1500 "player:alice"
ZADD leaderboard:weekly 2300 "player:bob"
ZADD leaderboard:weekly 1800 "player:carol"
ZADD leaderboard:weekly 3100 "player:dave"
ZADD leaderboard:weekly 900  "player:eve"

# ZINCRBY: incremento atomico dello score (ogni kill += 100 punti)
ZINCRBY leaderboard:weekly 100 "player:alice"   # nuovo score: 1600

# ZRANK: posizione 0-based (dal basso, score crescente)
ZRANK leaderboard:weekly "player:bob"    # 2 (0-based)

# ZREVRANK: posizione dal top (score decrescente)
ZREVRANK leaderboard:weekly "player:dave"  # 0 (e' primo!)
ZREVRANK leaderboard:weekly "player:bob"   # 2

# ZSCORE: score di un membro specifico
ZSCORE leaderboard:weekly "player:carol"   # "1800"

# ZREVRANGE: top N giocatori (posizione, score decrescente)
ZREVRANGE leaderboard:weekly 0 4 WITHSCORES
# 1) "player:dave"
# 2) "3100"
# 3) "player:bob"
# 4) "2300"
# 5) "player:carol"
# 6) "1800"
# 7) "player:alice"
# 8) "1600"
# 9) "player:eve"
# 10) "900"

# ZRANGEBYSCORE: giocatori tra 1000 e 2000 punti
ZRANGEBYSCORE leaderboard:weekly 1000 2000 WITHSCORES
# 1) "player:alice"
# 2) "1600"
# 3) "player:carol"
# 4) "1800"

# ZCOUNT: quanti giocatori con score >= 1500
ZCOUNT leaderboard:weekly 1500 +inf    # 3

# ZCARD: totale membri nel sorted set
ZCARD leaderboard:weekly    # 5
# Python: leaderboard con Sorted Sets
import redis
from datetime import datetime

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

class Leaderboard:
    def __init__(self, name: str):
        self.key = f"leaderboard:{name}"

    def add_score(self, player: str, score: float) -> float:
        """Aggiunge punti al giocatore, restituisce nuovo totale."""
        return r.zincrby(self.key, score, player)

    def get_rank(self, player: str) -> int | None:
        """Posizione del giocatore (1-based, top = 1)."""
        rank = r.zrevrank(self.key, player)
        return rank + 1 if rank is not None else None

    def get_top(self, n: int = 10) -> list[dict]:
        """Top N giocatori con score."""
        entries = r.zrevrange(self.key, 0, n - 1, withscores=True)
        return [
            {'player': player, 'score': score, 'rank': i + 1}
            for i, (player, score) in enumerate(entries)
        ]

    def get_around(self, player: str, delta: int = 2) -> list[dict]:
        """I delta giocatori sopra e sotto un dato giocatore."""
        rank = r.zrevrank(self.key, player)
        if rank is None:
            return []
        start = max(0, rank - delta)
        end = rank + delta
        entries = r.zrevrange(self.key, start, end, withscores=True)
        return [
            {'player': p, 'score': s, 'rank': start + i + 1}
            for i, (p, s) in enumerate(entries)
        ]

# Uso
lb = Leaderboard("weekly")
lb.add_score("player:alice", 1500)
lb.add_score("player:bob", 2300)
lb.add_score("player:carol", 1800)
lb.add_score("player:dave", 3100)

print(lb.get_top(3))
# [{'player': 'player:dave', 'score': 3100.0, 'rank': 1}, ...]

print(lb.get_rank("player:carol"))  # 2
print(lb.get_around("player:bob", delta=1))  # bob + dave + carol

HyperLogLog: Sabit Bellekle Kardinalite Sayımı

HyperLogLog, kardinaliteyi tahmin etmek için olasılıksal bir çerçevedir bir miktar bellek kullanarak bir kümenin (kaç tane farklı öğenin olduğu) devamlı: Veri kümesi boyutundan bağımsız olarak 12KB. hata standart yaklaşık %0,81'dir. O sana söyleyemez Hangi gördüğü unsurlar yalnız kaç tane belirgin.

# HyperLogLog: conteggio unique views

# PFADD: aggiunge elementi all'HLL
PFADD page:article-100:views "user:alice" "user:bob" "user:carol"
PFADD page:article-100:views "user:alice"  # Duplicato: ignorato nella stima

# PFCOUNT: stima del numero di elementi distinti
PFCOUNT page:article-100:views    # 3 (stima, non esatto)

# Aggiunta in batch
PFADD page:article-100:views "user:dave" "user:eve" "user:frank"
PFCOUNT page:article-100:views    # 6

# PFMERGE: unisce piu HLL in uno (unique across multiple sets)
PFADD page:article-200:views "user:alice" "user:george" "user:henry"
PFMERGE all-articles page:article-100:views page:article-200:views
PFCOUNT all-articles    # ~8 (alice contata una sola volta nell'unione)

# Pattern: daily unique visitors
# Chiave per giorno: views:2026-03-20
PFADD views:2026-03-20 "user:alice"
PFADD views:2026-03-20 "user:bob"
# ... milioni di utenti, sempre 12KB

# Weekly count: merge dei 7 giorni
PFMERGE views:week-12 \
  views:2026-03-14 views:2026-03-15 views:2026-03-16 views:2026-03-17 \
  views:2026-03-18 views:2026-03-19 views:2026-03-20
PFCOUNT views:week-12    # Unique visitors della settimana
# Python: tracking unique page views con HyperLogLog
import redis
from datetime import date

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

def track_page_view(article_id: int, user_id: str) -> None:
    """Registra una view di un articolo da un utente."""
    today = date.today().isoformat()
    # Chiave giornaliera per articolo
    daily_key = f"hll:article:{article_id}:{today}"
    r.pfadd(daily_key, user_id)
    r.expire(daily_key, 90 * 86400)  # TTL 90 giorni

def get_unique_views(article_id: int, since_date: date, until_date: date) -> int:
    """Unique views in un range di date."""
    keys = []
    current = since_date
    while current <= until_date:
        keys.append(f"hll:article:{article_id}:{current.isoformat()}")
        current = date.fromordinal(current.toordinal() + 1)

    if not keys:
        return 0

    # Merge temporaneo per ottenere il conteggio dell'intero range
    temp_key = f"hll:temp:{article_id}:{since_date}:{until_date}"
    r.pfmerge(temp_key, *keys)
    r.expire(temp_key, 60)  # Cache il risultato per 60s
    return r.pfcount(temp_key)

# Track views
track_page_view(100, "user:alice")
track_page_view(100, "user:bob")
track_page_view(100, "user:alice")  # Secondo accesso: non conta

from datetime import date
views = get_unique_views(100, date(2026, 3, 1), date(2026, 3, 20))
print(f"Unique views marzo: ~{views}")

# Confronto memoria: Set vs HLL per 1 milione di utenti
# Redis Set: ~50MB (64 byte per elemento)
# HyperLogLog: 12KB fissi (4000x piu efficiente)

Jeo-uzaysal: Jeo-uzaysal İndeks ile Yakınlık Aramaları

Redis Jeo-Uzamsal Dizini dahili olarak puanın şu şekilde olduğu bir Sıralanmış Küme kullanır: geohash'ı koordine edin. GEOADD, GEORADIUS ve GEODIST şunları yapmanızı sağlar O(N + log M) karmaşıklığında yakınlık araması; burada N, alan ve M elemanların toplamı ile sonuçlanır.

# Redis Geospatial: trova ristoranti vicini

# GEOADD key longitude latitude member
GEOADD restaurants 9.1859 45.4654 "ristorante-dal-mario"
GEOADD restaurants 9.1900 45.4680 "trattoria-lombarda"
GEOADD restaurants 9.1750 45.4600 "pizzeria-napoli"
GEOADD restaurants 9.2100 45.4800 "sushi-bento"
GEOADD restaurants 9.1850 45.4660 "bar-centrale"

# GEODIST: distanza tra due punti
GEODIST restaurants "ristorante-dal-mario" "trattoria-lombarda" km
# "0.3821" (circa 382 metri)

# GEOPOS: coordinate di un membro
GEOPOS restaurants "ristorante-dal-mario"
# 1) 1) "9.18589949607849121"
#    2) "45.46539883597492027"

# GEOSEARCH (Redis 6.2+): sostituisce GEORADIUS deprecato
# Trova ristoranti entro 500m dalla posizione corrente
GEOSEARCH restaurants
  FROMMEMBER "bar-centrale"
  BYRADIUS 500 m
  ASC
  COUNT 5
  WITHCOORD WITHDIST

# Oppure da coordinate GPS
GEOSEARCH restaurants
  FROMLONLAT 9.1860 45.4655
  BYRADIUS 1 km
  ASC
  COUNT 10

# GEOHASH: hash della posizione (per indicizzazione esterna)
GEOHASH restaurants "ristorante-dal-mario"
# 1) "u0nd0swfxh0"
# Python: proximity search per delivery app
import redis
from dataclasses import dataclass

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

@dataclass
class Restaurant:
    name: str
    longitude: float
    latitude: float
    category: str

def index_restaurant(restaurant: Restaurant) -> None:
    """Aggiunge ristorante all'indice geospaziale."""
    r.geoadd('restaurants:geo', {
        restaurant.name: (restaurant.longitude, restaurant.latitude)
    })
    # Salva metadata in un Hash separato
    r.hset(f"restaurant:{restaurant.name}", mapping={
        'name': restaurant.name,
        'category': restaurant.category,
        'lon': str(restaurant.longitude),
        'lat': str(restaurant.latitude),
    })

def find_nearby(lon: float, lat: float, radius_km: float, limit: int = 10) -> list[dict]:
    """Trova ristoranti entro radius_km dalla posizione."""
    results = r.geosearch(
        'restaurants:geo',
        longitude=lon,
        latitude=lat,
        radius=radius_km,
        unit='km',
        sort='ASC',
        count=limit,
        withdist=True,
        withcoord=True,
    )

    restaurants = []
    for entry in results:
        name, dist, (res_lon, res_lat) = entry
        metadata = r.hgetall(f"restaurant:{name}")
        restaurants.append({
            'name': name,
            'distance_km': round(dist, 3),
            'coordinates': {'lon': res_lon, 'lat': res_lat},
            'category': metadata.get('category', ''),
        })

    return restaurants

# Popola indice
for restaurant in [
    Restaurant("dal-mario", 9.1859, 45.4654, "italiana"),
    Restaurant("trattoria-lombarda", 9.1900, 45.4680, "italiana"),
    Restaurant("sushi-bento", 9.2100, 45.4800, "giapponese"),
]:
    index_restaurant(restaurant)

# Cerca ristoranti entro 1km da piazza Duomo Milano
nearby = find_nearby(lon=9.1895, lat=45.4654, radius_km=1.0)
for r_info in nearby:
    print(f"{r_info['name']}: {r_info['distance_km']}km ({r_info['category']})")

Hangi Veri Yapısı Ne Zaman Kullanılmalı

Hızlı Seçim Kılavuzu

  • Sicim: Basit değerler, sayaçlar, işaretler, JWT belirteçleri, önbelleğe alınmış yanıtlar
  • Doğramak: Bireysel alanlara erişimi olan yapılandırılmış nesneler (kullanıcılar, oturumlar, ürünler)
  • Liste: FIFO/LIFO kuyrukları, etkinlik yayınları, eklemeye göre sıralanmış günlükler
  • Ayarlamak: Etiketler, çoktan çoğa ilişkiler, üyelik kontrolleri, ayarlama işlemleri
  • Sıralanmış Küme: Skor tablosu, öncelik sırası, sıralı zaman çizelgesi, aralık sorguları
  • HyperLogLog: Sabit bellekle yaklaşık benzersiz sayı (görüntülemeler, ziyaretçiler)
  • Jeouzaysal: Yakınlık araması, teslimat yarıçapı, "yakınımdaki" özellikler
  • Bitmapler: Kullanıcı başına özellik işaretleri, günlük varlık takibi (DAU)
  • Akışlar: Kalıcı olay günlüğü, tüketici gruplarıyla mesaj kuyruğu

Kaçınılması Gereken Anti-Desenler

# ANTI-PATTERN 1: HGETALL su hash enormi
# Se un hash ha 10.000 campi, HGETALL porta tutto in memoria del client
# Usa HSCAN per iterare in modo sicuro
cursor = 0
while True:
    cursor, fields = r.hscan('big-hash', cursor, count=100)
    # processa fields
    if cursor == 0:
        break

# ANTI-PATTERN 2: SMEMBERS su set molto grandi
# SMEMBERS blocca Redis per la durata della risposta
# Usa SSCAN invece
cursor = 0
while True:
    cursor, members = r.sscan('huge-set', cursor, count=100)
    # processa members
    if cursor == 0:
        break

# ANTI-PATTERN 3: KEYS * in produzione
# KEYS * blocca Redis finche non completa la scansione
# Usa SCAN con pattern
cursor = 0
while True:
    cursor, keys = r.scan(cursor, match='user:*', count=100)
    # processa keys
    if cursor == 0:
        break

# ANTI-PATTERN 4: Usare HLL quando hai bisogno dell'insieme esatto
# HLL non puo dirti QUALI elementi ha visto, solo QUANTI (approssimativamente)
# Se hai bisogno di sapere "questo utente ha visto questo articolo?",
# usa un Set o un Bloom Filter (RedisBloom)

Sonuçlar

Redis'in gücü, doğru veri yapısını sorunla eşleştirmektir. Skor tablosu için Sıralanmış Küme, uygulama mantığını sıfıra indirir: Redis sıralamayı otomatik olarak korur. Benzersiz görünümler için HyperLogLog 50MB yerine 12KB kullanın. Yakınlık araması için Jeo-uzaysal Dizin koddaki trigonometrik hesaplamalara olan ihtiyacı ortadan kaldırır. Sonraki makalede Pub/Sub ve Streams adlı iki mod incelenecek Redis'in çok farklı teslimat garantileriyle mesajlaşması.

Redis Serisinde Gelecek Makaleler

  • Madde 2: Pub/Sub ve Redis Akışları — Tüketici Grupları ve Dağıtılmış İşleme
  • Madde 3: Lua Scripting — Atomik İşlemler, EVAL ve Redis İşlevleri
  • Madde 4: Hız Sınırlama - Jeton Kovası, Kayar Pencere ve Sabit Sayaç