高度な Redis データ構造: ハッシュ、セット、ソート セット、HyperLogLog、および地理空間
文字列を超えた Redis データ構造のマスター: リーダーボードにソート セットを使用する方法 リアルタイム、おおよそのカーディナリティをカウントするための HyperLogLog、および地理空間インデックス O(log N) の複雑さによる地理的近接検索の場合。
Redis は単なるキャッシュではありません
Redis は、「キャッシュとして使用されるメモリ内のキーと値のストア」とよく説明されます。 この定義は最も一般的なユースケースを捉えていますが、それを完全に曖昧にしています。 システムの真の力。 Redis 7.x は 10 種類のネイティブ データ構造を提供します。 それぞれが特定のアクセス パターンに合わせて最適化されているため、データをモデル化する必要はありません レポートまたはドキュメントでは、クエリをすでに反映した形式で保存します。
何を学ぶか
- ハッシュ: 個々のフィールドへの O(1) アクセスを持つモデル構造化オブジェクト
- セットとソートセット: セット、交差、スコアによるソート
- ZADD、ZRANK、ZRANGEBYSCORE によるリアルタイムのリーダーボード
- HyperLogLog: 固定 12KB のおおよそのカーディナリティ数
- 地理空間: 近接検索用の GEOADD、GEORADIUS、および GEODIST
- 各構造をいつ使用するか、どれを避けるべきか
ハッシュ: Redis の構造化オブジェクト
Redis ハッシュは、単一のキーに関連付けられたフィールドと値のペアのマップです。 これはオブジェクト (ユーザー、製品、セッション) を表す自然な方法です。 すべてを JSON にシリアル化する必要はありません。主な利点: 読み取りまたは更新ができる オブジェクト全体をロードせずに、O(1) の単一フィールドをロードします。
# 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'
セット: 固有のセットとセットに対する操作
Redis セットは、順序付けされていない一意の文字列のコレクションです。セットの強さ 集合間の演算にあります: SUNION、SINTER、SDIFF 和集合、積集合を計算します。 データをクライアントに持ち込む必要がなく、サーバー側での違いがわかります。
# 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
ソートされたセット: リーダーボードと範囲クエリ
ソート セットは、Redis で最も汎用性の高いデータ構造です。あらゆる要素 1つあります スコア 関連するフロート;セットは自動的に並べ替えられます スコアによって。ポジション(ランク)またはスコア範囲ごとに要素を読み取ることができ、 すべて O(log N) になります。リーダーボード、タイムライン、キューには自然な選択です 優先順位および範囲ベースのフィルタリング。
# 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: 定数メモリを使用したカーディナリティ カウント
HyperLogLog はカーディナリティを推定するための確率的フレームワークです メモリ量を使用したセット (個別の要素がいくつあるか) 絶え間ない: データセットのサイズに関係なく 12KB。間違い 標準は約0.81%です。彼はあなたに言えません どれの 彼が見た要素、 一人で 幾つか 明確な。
# 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)
地理空間: 地理空間インデックスを使用した近接検索
Redis 地理空間インデックスは、スコアが ジオハッシュをコーディネートします。 GEOADD、GEORADIUS、GEODIST を使用すると、次のことが可能になります。 複雑さ O(N + log M) の近接検索 (N は数) 結果は面積、M は要素の合計になります。
# 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']})")
いつどのデータ構造を使用するか
クイック選択ガイド
- 弦: 単純な値、カウンター、フラグ、JWT トークン、キャッシュされた応答
- ハッシュ: 個々のフィールドにアクセスできる構造化オブジェクト (ユーザー、セッション、製品)
- リスト: FIFO/LIFO キュー、アクティビティ フィード、挿入順に並べ替えられたログ
- セット: タグ、多対多の関係、メンバーシップ チェック、セット操作
- ソートされたセット: リーダーボード、優先キュー、順序付きタイムライン、範囲クエリ
- ハイパーログログ: 一定のメモリを使用したおおよその一意の数 (ビュー、訪問者)
- 地理空間: 近隣検索、配送範囲、「近く」機能
- ビットマップ: ユーザーごとの機能フラグ、毎日のプレゼンス追跡 (DAU)
- ストリーム: 永続的なイベント ログ、コンシューマ グループのメッセージ キュー
避けるべきアンチパターン
# 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)
結論
Redis の威力は、適切なデータ構造を問題に適合させることです。 リーダーボードのソート セットにより、アプリケーション ロジックがゼロになります: Redis ソートを自動的に維持します。独自のビューのための HyperLogLog 50MB の代わりに 12KB を使用します。近接検索のための地理空間インデックス コード内で三角関数の計算を行う必要がなくなります。 次の記事では、Pub/Sub と Streams の 2 つのモードについて説明します。 非常に異なる配信保証を持つ Redis メッセージングの。
Redis シリーズの今後の記事
- 第2条: Pub/Sub と Redis ストリーム — コンシューマ グループと分散処理
- 第3条: Lua スクリプト — アトミック オペレーション、EVAL、Redis 関数
- 第4条: レート制限 — トークンバケット、スライディングウィンドウ、固定カウンター







