跳转至

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