Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Redisデータ構造パターン、キャッシング戦略、分散ロック、レート制限、Pub/Sub、本番アプリケーション用コネクション管理。
.claude/skills/affaan-m-redis-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 127% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 84% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 75% | 0% |
| case-06 | ✓→✓ | = Same ✓ | 79% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 113% | 0% |
一般的なバックエンド使用例に対するRedisベストプラクティスの参考資料。
Redisはメモリ内データ構造ストアで、文字列、ハッシュ、リスト、セット、ソート済みセット、ストリームなどをサポートします。単一インスタンスでは個々のRedisコマンドは原子的ですが、マルチステップワークフローはLuaスクリプト、MULTI/EXECトランザクション、または明示的な同期化が必要です。RDBスナップショットまたはAOFログを通じてデータをオプションで永続化します。クライアントはRESPプロトコルを使用してTCP経由で通信します。接続プール不可欠でリクエストごとのハンドシェイクオーバーヘッドを回避します。
| Use Case | Structure | Example Key | |----------|-----------|-------------| | Simple cache | String | product:123 | | User session | Hash | session:abc | | Leaderboard | Sorted Set | scores:weekly | | Unique visitors | Set | visitors:2024-01-01 | | Activity feed | List | feed:user:456 | | Event stream | Stream | events:orders | | Counters / rate limits | String (INCR) | ratelimit:user:123 | | Bloom filter / HLL | HyperLogLog | hll:pageviews |
pythonimport redis import json r = redis.Redis(host='localhost', port=6379, decode_responses=True) def get_product(product_id: int): cache_key = f"product:{product_id}" cached = r.get(cache_key) if cached: return json.loads(cached) product = db.query("SELECT * FROM products WHERE id = %s", product_id) r.setex(cache_key, 3600, json.dumps(product)) # TTL: 1 hour return product
pythondef update_product(product_id: int, data: dict): # DB書き込み先 db.execute("UPDATE products SET ... WHERE id = %s", product_id) # キャッシュを即座に更新 cache_key = f"product:{product_id}" r.setex(cache_key, 3600, json.dumps(data))
python# タグベース削除 — セット内で関連キーをグループ化 def cache_product(product_id: int, category_id: int, data: dict): key = f"product:{product_id}" tag = f"tag:category:{category_id}" pipe = r.pipeline(transaction=True) pipe.setex(key, 3600, json.dumps(data)) pipe.sadd(tag, key) pipe.expire(tag, 3600) pipe.execute() def invalidate_category(category_id: int): tag = f"tag:category:{category_id}" keys = r.smembers(tag) if keys: r.delete(*keys) r.delete(tag)
pythonimport time import uuid def create_session(user_id: int, ttl: int = 86400) -> str: session_id = str(uuid.uuid4()) key = f"session:{session_id}" pipe = r.pipeline(transaction=True) pipe.hset(key, mapping={ "user_id": user_id, "created_at": int(time.time()), }) pipe.expire(key, ttl) pipe.execute() return session_id def get_session(session_id: str) -> dict | None: data = r.hgetall(f"session:{session_id}") return data if data else None def delete_session(session_id: str): r.delete(f"session:{session_id}")
pythondef is_rate_limited(user_id: int, limit: int = 100, window: int = 60) -> bool: key = f"ratelimit:{user_id}:{int(time.time()) // window}" pipe = r.pipeline(transaction=True) pipe.incr(key) pipe.expire(key, window) count, _ = pipe.execute() return count > limit
lua-- sliding_window.lua local key = KEYS[1] local now = tonumber(ARGV[1]) local window = tonumber(ARGV[2]) local limit = tonumber(ARGV[3]) redis.call('ZREMRANGEBYSCORE', key, 0, now - window) local count = redis.call('ZCARD', key) if count < limit then -- Use unique member (now + sequence) to avoid collisions within the same millisecond local seq_key = key .. ':seq' local seq = redis.call('INCR', seq_key) redis.call('EXPIRE', seq_key, math.ceil(window / 1000)) redis.call('ZADD', key, now, now .. '-' .. seq) redis.call('EXPIRE', key, math.ceil(window / 1000)) return 1 end return 0
pythonsliding_window = r.register_script(open('sliding_window.lua').read()) def allow_request(user_id: int) -> bool: key = f"ratelimit:sliding:{user_id}" now = int(time.time() * 1000) return bool(sliding_window(keys=[key], args=[now, 60000, 100]))
pythonimport uuid def acquire_lock(resource: str, ttl_ms: int = 5000) -> str | None: lock_key = f"lock:{resource}" token = str(uuid.uuid4()) acquired = r.set(lock_key, token, px=ttl_ms, nx=True) return token if acquired else None def release_lock(resource: str, token: str) -> bool: release_script = """ if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end """ result = r.eval(release_script, 1, f"lock:{resource}", token) return bool(result) # Usage token = acquire_lock("order:payment:123") if token: try: process_payment() finally: release_lock("order:payment:123", token)
> マルチノード設定の場合、フルRedlockアルゴリズムを実装する redlock-py ライブラリを使用してください。
python# Publisher def publish_event(channel: str, payload: dict): r.publish(channel, json.dumps(payload)) # Subscriber (blocking — run in separate thread/process) def subscribe_events(channel: str): pubsub = r.pubsub() pubsub.subscribe(channel) for message in pubsub.listen(): if message['type'] == 'message': handle(json.loads(message['data']))
python# Producer def emit(stream: str, event: dict): r.xadd(stream, event, maxlen=10000) # Cap stream length # Consumer group — guarantees at-least-once delivery try: r.xgroup_create('events:orders', 'processor', id='0', mkstream=True) except Exception: pass # Group already exists def consume(stream: str, group: str, consumer: str): while True: messages = r.xreadgroup(group, consumer, {stream: '>'}, count=10, block=2000) for _, entries in (messages or []): for msg_id, data in entries: process(data) r.xack(stream, group, msg_id)
> 配信保証、コンシューマーグループ、または再生が必要な場合、Pub/Sub代わりにStreamsを優先してください。
# Pattern: resource:id:field
user:123:profile
order:456:status
cache:product:789
# Pattern: namespace:resource:id
myapp:session:abc123
myapp:ratelimit:user:123
# Pattern: resource:date (time-bound keys)
stats:pageviews:2024-01-01| Data Type | Suggested TTL | |-----------|--------------| | User session | 24h (86400) | | API response cache | 5–15 min | | Rate limit window | Match window size | | Short-lived tokens | 5–10 min | | Leaderboard | 1h–24h | | Static/reference data | 1h–1 week |
常にTTLを設定してください。TTLなしのキーは無限に蓄積してメモリ圧力を引き起こします。
pythonfrom redis import ConnectionPool, Redis pool = ConnectionPool( host='localhost', port=6379, db=0, max_connections=20, decode_responses=True, socket_connect_timeout=2, socket_timeout=2, ) r = Redis(connection_pool=pool)
pythonfrom redis.cluster import RedisCluster r = RedisCluster( startup_nodes=[{"host": "redis-1", "port": 6379}], decode_responses=True, skip_full_coverage_check=True, )
pythonfrom redis.sentinel import Sentinel sentinel = Sentinel( [('sentinel-1', 26379), ('sentinel-2', 26379)], socket_timeout=0.5, ) master = sentinel.master_for('mymaster', decode_responses=True) replica = sentinel.slave_for('mymaster', decode_responses=True)
| Policy | Behavior | Best For | |--------|----------|----------| | noeviction | Error on write when full | Queues / critical data | | allkeys-lru | Evict least recently used | General cache | | volatile-lru | LRU only among keys with TTL | Mixed data store | | allkeys-lfu | Evict least frequently used | Skewed access patterns | | volatile-ttl | Evict soonest-to-expire | Prioritize long-lived data |
redis.confを通じて設定:maxmemory-policy allkeys-lru
| Anti-Pattern | Problem | Fix | |---|---|---| | Keys with no TTL | Memory grows unbounded | Always set TTL | | KEYS * in production | Blocks the server (O(N)) | Use SCAN cursor | | Storing large blobs (>100KB) | Slow serialization, memory pressure | Store reference + fetch from object store | | Single Redis for everything | No isolation between cache & queue | Use separate DBs or instances | | Ignoring connection pool limits | Connection exhaustion under load | Size pool to workload | | Not handling cache miss stampede | Thundering herd on cold start | Use locks or probabilistic early expiry | | FLUSHALL without thought | Wipes entire instance | Scope deletes by key pattern |
pythonimport threading _locks: dict[str, threading.Lock] = {} _locks_mutex = threading.Lock() def get_with_lock(key: str, fetch_fn, ttl: int = 300): cached = r.get(key) if cached: return json.loads(cached) with _locks_mutex: if key not in _locks: _locks[key] = threading.Lock() lock = _locks[key] with lock: cached = r.get(key) # Re-check after acquiring lock if cached: return json.loads(cached) value = fetch_fn() r.setex(key, ttl, json.dumps(value)) return value
> マルチプロセスデプロイメント:インプロセスロックを上記の分散ロックセクション から acquire_lock/release_lock に置き換えてください。
Django/Flask APIエンドポイントにキャッシング追加: レスポンスに5分TTLでCache-asideを使用。リクエストパラメータでキーを指定。
ユーザーごとにAPIレート制限: 低トラフィックエンドポイントに固定ウィンドウを pipeline(transaction=True) で使用;正確なユーザーごと制限にはsliding-windowの Lua使用。
ワーカー間のバックグラウンドジョブ調整: 予想ジョブ期間を超えるTTLで acquire_lock を使用。常に finally ブロックでリリース。
複数購読者への通知のファンアウト: ファイアアンドフォーゲットにPub/Subを使用。保証配信または再生が必要な場合、Streamsに切り替え。
| Pattern | When to Use | |---------|-------------| | Cache-aside | Read-heavy, tolerate slight staleness | | Write-through | Strong consistency required | | Distributed lock | Prevent concurrent access to a resource | | Sliding window rate limit | Accurate per-user throttling | | Redis Streams | Durable event queue with consumer groups | | Pub/Sub | Broadcast with no delivery guarantees needed | | Sorted Set leaderboard | Ranked scoring, pagination | | HyperLogLog | Approximate unique count at low memory |
postgres-patterns — リレーショナルデータパターンbackend-patterns — APIおよびサービスレイヤーパターンdatabase-migrations — スキーマバージョニングdjango-patterns — Djangoキャッシュフレームワーク統合database-reviewer — 全データベースレビューワークフロー| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-05 | pass→pass | 16,775 | 10,011 | -40% | 1 | 1 | 0% | 3,027 | 5,292 | +75% | 0 | 0 | — |
case-06 | pass→pass | 18,698 | 17,355 | -7% | 1 | 1 | 0% | 3,737 | 6,692 | +79% | 0 | 0 | — |
case-01 | fail→fail | 29,924 | 14,901 | -50% | 1 | 1 | 0% | 3,621 | 6,320 | +75% | 0 | 0 | — |
case-02 | pass→pass | 13,303 | 10,485 | -21% | 1 | 1 | 0% | 2,516 | 5,367 | +113% | 0 | 0 | — |
case-03 | pass→pass | 14,858 | 13,045 | -12% | 1 | 1 | 0% | 2,689 | 5,511 | +105% | 0 | 0 | — |
case-04 | fail→pass | 14,146 | 13,933 | -2% | 1 | 1 | 0% | 2,756 | 6,245 | +127% | 0 | 0 | — |
case-07 | fail→pass | 17,196 | 10,475 | -39% | 1 | 1 | 0% | 2,929 | 5,399 | +84% | 0 | 0 | — |
case-08 | pass→pass | 9,043 | 6,515 | -28% | 1 | 1 | 0% | 1,511 | 4,505 | +198% | 0 | 0 | — |
case-09 | pass→pass | 5,598 | 7,677 | +37% | 1 | 1 | 0% | 932 | 4,057 | +335% | 0 | 0 | — |
case-10 | pass→pass | 11,191 | 8,064 | -28% | 1 | 1 | 0% | 1,654 | 4,774 | +189% | 0 | 0 | — |
case-11 | pass→pass | 11,582 | 9,886 | -15% | 1 | 1 | 0% | 2,164 | 5,189 | +140% | 0 | 0 | — |
case-12 | pass→pass | 26,293 | 9,640 | -63% | 1 | 1 | 0% | 2,119 | 5,163 | +144% | 0 | 0 | — |
case-13 | pass→pass | 11,738 | 10,327 | -12% | 1 | 1 | 0% | 2,110 | 5,443 | +158% | 0 | 0 | — |
case-14 | pass→pass | 10,521 | 5,908 | -44% | 1 | 1 | 0% | 2,126 | 4,584 | +116% | 0 | 0 | — |
case-15 | pass→pass | 18,196 | 9,256 | -49% | 1 | 1 | 0% | 3,227 | 5,085 | +58% | 0 | 0 | — |
case-16 | pass→pass | 11,783 | 11,210 | -5% | 1 | 1 | 0% | 2,050 | 5,530 | +170% | 0 | 0 | — |
case-17 | fail→fail | 11,379 | 8,842 | -22% | 1 | 1 | 0% | 2,033 | 5,133 | +152% | 0 | 0 | — |
case-18 | pass→pass | 9,260 | 7,194 | -22% | 1 | 1 | 0% | 1,628 | 4,856 | +198% | 0 | 0 | — |
case-19 | pass→pass | 6,898 | 7,906 | +15% | 1 | 1 | 0% | 1,215 | 4,840 | +298% | 0 | 0 | — |
case-20 | pass→pass | 13,529 | 13,637 | +1% | 1 | 1 | 0% | 2,314 | 5,704 | +146% | 0 | 0 | — |
case-21 | pass→pass | 14,432 | 12,371 | -14% | 1 | 1 | 0% | 2,547 | 5,742 | +125% | 0 | 0 | — |
case-22 | pass→pass | 9,251 | 8,988 | -3% | 1 | 1 | 0% | 1,652 | 4,862 | +194% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.