Redis Patterns
Core Data Structures
# Strings — counters, cache
SET user:1:name "Alice" EX 3600
INCR page:views
SETNX lock:resource 1 # atomic set if not exists
# Hashes — objects
HSET user:1 name "Alice" email "a@b.com" age 30
HGETALL user:1
HINCRBY user:1 login_count 1
# Lists — queues, feeds
LPUSH queue:jobs task1 task2 # push left
BRPOP queue:jobs 30 # blocking pop right
LRANGE feed:user:1 0 49 # paginate
# Sets — unique items, social graphs
SADD user:1:friends 2 3 4
SINTERSTORE common_friends user:1:friends user:2:friends
# Sorted Sets — leaderboards, priority queues
ZADD leaderboard 1500 "user:1"
ZREVRANGE leaderboard 0 9 WITHSCORES # top 10
ZRANGEBYSCORE events 1700000000 1700086400 # time window
Pub/Sub and Streams
# Pub/Sub (fire and forget — no persistence)
SUBSCRIBE channel:notifications
PUBLISH channel:notifications '{"type":"alert","msg":"hello"}'
# Streams — persistent, consumer groups
XADD events:stream * type "click" user_id "42"
XGROUP CREATE events:stream workers $ MKSTREAM
XREADGROUP GROUP workers consumer1 COUNT 10 BLOCK 2000 STREAMS events:stream >
XACK events:stream workers <message-id>
# Python stream consumer
import redis
r = redis.Redis()
while True:
msgs = r.xreadgroup('GROUP', 'workers', 'c1', count=10,
block=2000, streams={'events:stream': '>'})
for stream, messages in (msgs or []):
for msg_id, data in messages:
process(data)
r.xack('events:stream', 'workers', msg_id)
Lua Scripting (Atomic Operations)
-- Rate limiter (sliding window)
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
redis.call('ZADD', key, now, now)
redis.call('EXPIRE', key, window / 1000)
return 1
end
return 0
rate_limit_script = r.register_script(lua_script)
allowed = rate_limit_script(
keys=[f'rate:{user_id}'],
args=[int(time.time() * 1000), 60000, 100] # 100 req/min
)
Caching Patterns
# Cache-aside (most common)
def get_user(user_id):
cached = r.get(f'user:{user_id}')
if cached:
return json.loads(cached)
user = db.query(User).get(user_id)
r.setex(f'user:{user_id}', 300, json.dumps(user.to_dict()))
return user
# Cache stampede prevention (probabilistic early expiration)
def get_with_lock(key, ttl, fetch_fn):
val = r.get(key)
if val:
return json.loads(val)
lock_key = f'lock:{key}'
if r.set(lock_key, 1, nx=True, ex=10):
val = fetch_fn()
r.setex(key, ttl, json.dumps(val))
r.delete(lock_key)
return val
time.sleep(0.1)
return get_with_lock(key, ttl, fetch_fn)
Cluster and Persistence
# Cluster — 16384 hash slots across nodes
redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 \
127.0.0.1:7002 --cluster-replicas 1
# Hash tags for multi-key atomicity in cluster
SET {user:1}:profile "..."
SET {user:1}:settings "..." # same slot as profile
# Persistence config
# RDB: snapshot every N seconds if M keys changed
save 900 1
save 300 10
# AOF: append-every-second (balance durability vs perf)
appendonly yes
appendfsync everysec
# INFO persistence
redis-cli INFO persistence
Common Patterns
# Distributed lock (Redlock)
from redis import Redis
from redlock import Redlock
dlm = Redlock([{"host": "redis1"}, {"host": "redis2"}])
lock = dlm.lock("resource", 10000) # 10s TTL
if lock:
try:
do_work()
finally:
dlm.unlock(lock)
# Session store
r.setex(f'session:{token}', 86400, json.dumps(session_data))
# Bloom filter (RedisBloom module)
r.execute_command('BF.ADD', 'seen_emails', 'user@example.com')
exists = r.execute_command('BF.EXISTS', 'seen_emails', 'user@example.com')