Redis 模式¶
常见后端使用场景的 Redis 最佳实践参考资料。
工作原理¶
Redis 是内存数据结构存储,支持字符串、哈希、列表、集合、有序集合、流等。在单个实例中,单个 Redis 命令是原子的,但多步骤工作流需要 Lua 脚本、MULTI/EXEC 事务或显式同步。可通过 RDB 快照或 AOF 日志可选地持久化数据。客户端使用 RESP 协议通过 TCP 通信。连接池不可或缺,可避免每次请求的握手开销。
何时启用¶
- 为应用添加缓存
- 实现速率限制或限流
- 构建分布式锁或协调
- 配置会话或令牌存储
- 使用 Pub/Sub 或 Redis Streams 进行消息传递
- 在生产环境中配置 Redis(连接池、淘汰、集群)
数据结构速查表¶
| 使用场景 | 结构 | 示例键 |
|---|---|---|
| 简单缓存 | 字符串 | product:123 |
| 用户会话 | 哈希 | session:abc |
| 排行榜 | 有序集合 | scores:weekly |
| 唯一访客 | 集合 | visitors:2024-01-01 |
| 动态信息流 | 列表 | feed:user:456 |
| 事件流 | 流 | events:orders |
| 计数器 / 速率限制 | 字符串 (INCR) | ratelimit:user:123 |
| 布隆过滤器 / HLL | HyperLogLog | hll:pageviews |
核心模式¶
旁路缓存(懒加载)¶
import 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
写穿透缓存¶
def 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))
缓存失效¶
# タグベース削除 — セット内で関連キーをグループ化
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)
会话存储¶
import 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}")
速率限制¶
固定窗口(简单)¶
def 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
sliding_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]))
分布式锁¶
分布式锁(单节点 — SET NX PX)¶
import 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库。
Pub/Sub 与 Streams¶
Pub/Sub(即发即忘)¶
# 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']))
Redis Streams(持久队列)¶
# 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)
如果需要投递保证、消费者组或重放,请优先使用 Streams 而非 Pub/Sub。
核心设计¶
命名约定¶
# 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
TTL 策略¶
| 数据类型 | 建议 TTL |
|---|---|
| 用户会话 | 24 小时(86400) |
| API 响应缓存 | 5–15 分钟 |
| 限流窗口 | 与窗口大小一致 |
| 短期令牌 | 5–10 分钟 |
| 排行榜 | 1–24 小时 |
| 静态/参考数据 | 1 小时–1 周 |
请始终设置 TTL。没有 TTL 的键会无限累积并导致内存压力。
连接管理¶
连接池¶
from 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)
集群模式¶
from redis.cluster import RedisCluster
r = RedisCluster(
startup_nodes=[{"host": "redis-1", "port": 6379}],
decode_responses=True,
skip_full_coverage_check=True,
)
Sentinel(高可用性)¶
from 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)
驱逐策略¶
| 策略 | 行为 | 适用场景 |
|---|---|---|
noeviction |
已满时写入报错 | 队列 / 关键数据 |
allkeys-lru |
淘汰最近最少使用 | 通用缓存 |
volatile-lru |
仅对设置了 TTL 的键进行 LRU | 混合数据存储 |
allkeys-lfu |
淘汰最不经常使用 | 偏斜访问模式 |
volatile-ttl |
淘汰即将过期的键 | 优先保留长期数据 |
通过 redis.conf 设置:maxmemory-policy allkeys-lru
反模式¶
| 反模式 | 问题 | 修复方案 |
|---|---|---|
| 无 TTL 的键 | 内存无限增长 | 始终设置 TTL |
在生产环境使用 KEYS * |
阻塞服务器(O(N)) | 使用 SCAN 游标 |
| 存储大型对象(>100KB) | 序列化缓慢,内存压力 | 存储引用 + 从对象存储获取 |
| 单个 Redis 用于所有场景 | 缓存与队列之间无隔离 | 使用独立的 DB 或实例 |
| 忽略连接池限制 | 负载下连接耗尽 | 根据工作负载调整池大小 |
| 未处理缓存未命中惊群 | 冷启动时惊群效应 | 使用锁或概率性提前过期 |
不加思考地执行 FLUSHALL |
清空整个实例 | 按键模式限定删除范围 |
缓存未命中惊群预防¶
import 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 端点添加缓存: 使用 Cache-aside,响应设置 5 分钟 TTL。使用请求参数指定键。
按用户进行 API 限流: 对低流量端点使用固定窗口加 pipeline(transaction=True);对于精确的按用户限制,使用滑动窗口的 Lua 脚本。
协调 Worker 之间的后台任务: 使用 acquire_lock,TTL 应超过预期任务持续时间。始终在 finally 块中释放。
向多个订阅者扇出通知: 使用 Pub/Sub 进行即发即忘。如果需要投递保证或重放,请切换到 Streams。
快速参考¶
| 模式 | 使用时机 |
|---|---|
| Cache-aside | 读密集,可容忍轻微过期 |
| Write-through | 需要强一致性 |
| 分布式锁 | 防止对资源的并发访问 |
| 滑动窗口限流 | 精确的按用户限流 |
| Redis Streams | 带消费者组的持久事件队列 |
| Pub/Sub | 无需投递保证的广播 |
| 有序集合排行榜 | 排名计分、分页 |
| HyperLogLog | 低内存下的近似唯一计数 |
相关¶
- Skill:
postgres-patterns— 关系型数据模式 - Skill:
backend-patterns— API 和服务层模式 - Skill:
database-migrations— Schema 版本管理 - Skill:
django-patterns— Django 缓存框架集成 - Agent:
database-reviewer— 所有数据库审查工作流
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 ev/ECC