Python の set vs Redis の Set 型(分散環境での集合)
Python の set はプロセス内で完結するインメモリのデータ構造だ。一方、Redis の Set は分散環境で複数のプロセスやサーバー間で共有できる。この違いがアーキテクチャ選択に大きく影響する。
基本的な違い
単一プロセス内のメモリに存在。プロセス終了で消失。高速だがスケールしない。
外部サーバーに存在。永続化可能。ネットワーク越しにアクセス。水平スケール可能。
Redis はデータベースとして機能し、Python set は単なるデータ構造だ。
Redis Set の基本操作
redis-py を使った基本操作を見る。
import redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# 要素の追加(SADD)
r.sadd('fruits', 'apple', 'banana', 'cherry')
# 存在確認(SISMEMBER)
print(r.sismember('fruits', 'apple')) # True
print(r.sismember('fruits', 'grape')) # False
# 全要素取得(SMEMBERS)
print(r.smembers('fruits')) # {'apple', 'banana', 'cherry'}
# 要素数(SCARD)
print(r.scard('fruits')) # 3
# 削除(SREM)
r.srem('fruits', 'banana')
操作名は異なるが、Python の set とほぼ対応している。
集合演算
Redis でも和集合、積集合、差集合ができる。
import redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
r.sadd('set_a', 1, 2, 3, 4)
r.sadd('set_b', 3, 4, 5, 6)
# 積集合(SINTER)
print(r.sinter('set_a', 'set_b')) # {'3', '4'}
# 和集合(SUNION)
print(r.sunion('set_a', 'set_b')) # {'1', '2', '3', '4', '5', '6'}
# 差集合(SDIFF)
print(r.sdiff('set_a', 'set_b')) # {'1', '2'}
結果を新しいキーに保存する SINTERSTORE, SUNIONSTORE, SDIFFSTORE もある。
# 結果を別のキーに保存
r.sinterstore('set_intersection', 'set_a', 'set_b')
print(r.smembers('set_intersection')) # {'3', '4'}
パフォーマンス特性
Redis へのアクセスはネットワーク越しになるため、レイテンシが発生する。
import redis
import time
r = redis.Redis(host='localhost', port=6379)
# ローカル set
local_set = set(range(10000))
start = time.perf_counter()
for i in range(1000):
i in local_set
print(f"Python set: {time.perf_counter() - start:.4f}s")
# Redis Set(事前に SADD 済み)
start = time.perf_counter()
for i in range(1000):
r.sismember('redis_set', i)
print(f"Redis Set: {time.perf_counter() - start:.4f}s")
ローカルの set はマイクロ秒単位だが、Redis は往復で数百マイクロ秒〜数ミリ秒かかる。単純な速度では Python set が圧倒的だ。
パイプラインによる最適化
複数のコマンドをまとめて送ることで、ネットワークのオーバーヘッドを削減できる。
import redis
r = redis.Redis(host='localhost', port=6379)
# パイプラインなし(遅い)
for i in range(1000):
r.sadd('slow_set', i)
# パイプラインあり(速い)
pipe = r.pipeline()
for i in range(1000):
pipe.sadd('fast_set', i)
pipe.execute()
パイプラインを使えば、1000 回の往復が 1 回になる。
Redis Set の強み
Python set では実現しにくい機能がある。
RDB スナップショットや AOF でディスクに保存。再起動後もデータが残る。
EXPIRE でキーに有効期限を設定。セッション管理やキャッシュに有用。
複数のアプリケーションサーバーから同じ Set にアクセス可能。
import redis
r = redis.Redis(host='localhost', port=6379)
# 有効期限付きの Set
r.sadd('session:user123', 'token_abc')
r.expire('session:user123', 3600) # 1時間後に自動削除
ランダム要素の取得
Redis には set からランダムに要素を取得する機能がある。
import redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
r.sadd('lottery', 'alice', 'bob', 'charlie', 'dave', 'eve')
# ランダムに 1 つ取得(削除しない)
print(r.srandmember('lottery'))
# ランダムに 2 つ取得
print(r.srandmember('lottery', 2))
# ランダムに 1 つ取得して削除(SPOP)
print(r.spop('lottery'))
Python の set で同様のことをするには、一度リストに変換して random.choice() を使う必要がある。
スキャンによる大規模 Set の走査
巨大な Set をイテレートするには SSCAN を使う。
import redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# 大量のデータを追加
for i in range(100000):
r.sadd('huge_set', f'item:{i}')
# SSCAN でカーソルベースの走査
cursor = 0
count = 0
while True:
cursor, items = r.sscan('huge_set', cursor, count=1000)
count += len(items)
if cursor == 0:
break
print(f"Total items: {count}")
SMEMBERS は全要素を一度に返すため、巨大な Set では Redis をブロックしてしまう。SSCAN なら少しずつ取得できる。
Sorted Set との違い
Redis には順序付きの Sorted Set(ZSET)もある。
import redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# Sorted Set(スコア付き)
r.zadd('ranking', {'alice': 100, 'bob': 85, 'charlie': 92})
# スコア順で取得
print(r.zrange('ranking', 0, -1, withscores=True))
# [('bob', 85.0), ('charlie', 92.0), ('alice', 100.0)]
# スコアでフィルタ
print(r.zrangebyscore('ranking', 90, 100))
# ['charlie', 'alice']
Python の set にはスコアの概念がなく、heapq や別のデータ構造が必要になる。
使い分けの指針
| 要件 | Python set | Redis Set |
|---|---|---|
| 単一プロセス内で完結 | ◎ | △ |
| 複数サーバーで共有 | × | ◎ |
| 永続化が必要 | × | ◎ |
| ミリ秒未満のレイテンシ | ◎ | × |
| 数百万要素の存在確認 | ◎ | ○ |
典型的なパターンとして、ローカルでの高速処理には Python set を使い、プロセス間・サーバー間での共有や永続化が必要な場合に Redis Set を使う。
import redis
# ハイブリッドアプローチ
class HybridSet:
def __init__(self, redis_key):
self.redis = redis.Redis(host='localhost', port=6379)
self.key = redis_key
self.local_cache = None
def load(self):
"""Redis から読み込んでローカルキャッシュ"""
self.local_cache = set(self.redis.smembers(self.key))
def __contains__(self, item):
"""高速な存在確認"""
if self.local_cache is not None:
return item in self.local_cache
return self.redis.sismember(self.key, item)
def add(self, item):
"""両方に追加"""
self.redis.sadd(self.key, item)
if self.local_cache is not None:
self.local_cache.add(item)
このようにローカルキャッシュと Redis を組み合わせることで、読み取りは高速に、書き込みは永続化されるという両方の利点を得られる。