Skip to content
清晨的一缕阳光
返回

Redis 缓存设计与优化

Redis 缓存设计与优化

缓存是提升系统性能的关键手段。Redis 作为高性能缓存系统,广泛应用于各种场景。本文将深入缓存设计模式,解析常见问题及解决方案。

一、缓存模式

1.1 Cache-Aside(旁路缓存)

最常用的缓存模式

读操作:
1. 先读缓存
2. 缓存未命中,读数据库
3. 写入缓存
4. 返回结果

写操作:
1. 更新数据库
2. 删除缓存

代码实现

def get_user(user_id):
    # 1. 读缓存
    key = f"user:{user_id}"
    user = redis.get(key)
    
    if user:
        return deserialize(user)
    
    # 2. 读数据库
    user = db.query("SELECT * FROM users WHERE id = ?", user_id)
    
    if user:
        # 3. 写缓存
        redis.setex(key, 300, serialize(user))
    
    return user

def update_user(user_id, data):
    # 1. 更新数据库
    db.execute("UPDATE users SET ? WHERE id = ?", data, user_id)
    
    # 2. 删除缓存
    redis.delete(f"user:{user_id}")

优点

缺点

1.2 Read/Write Through

读操作:
1. 应用读缓存
2. 缓存未命中,缓存系统读数据库
3. 缓存系统返回

写操作:
1. 应用写缓存
2. 缓存系统异步写数据库

特点

1.3 Write Behind

写操作:
1. 应用只写缓存
2. 缓存异步批量写数据库

特点

二、缓存问题与解决方案

2.1 缓存穿透

问题描述

查询不存在的数据

缓存未命中

查询数据库(也未命中)

每次请求都打到数据库

攻击场景:
恶意请求大量不存在的 key
导致数据库压力过大

解决方案

方案 1:布隆过滤器

from redis.commands.search.field import TextField
from redis.commands.search.indexDefinition import IndexDefinition
from redisbloom.client import Client

# 初始化布隆过滤器
bf = Client(redis)

# 添加元素
bf.add("bloom_users", "user_1001")
bf.add("bloom_users", "user_1002")

# 检查是否存在
if bf.exists("bloom_users", "user_1001"):
    # 可能存在,查询缓存/数据库
    user = get_user(1001)
else:
    # 一定不存在,直接返回
    return None

方案 2:缓存空值

def get_user(user_id):
    key = f"user:{user_id}"
    
    # 读缓存
    user = redis.get(key)
    if user == "__NULL__":
        return None
    
    if user:
        return deserialize(user)
    
    # 读数据库
    user = db.query("SELECT * FROM users WHERE id = ?", user_id)
    
    if not user:
        # 缓存空值,防止穿透
        redis.setex(key, 60, "__NULL__")
        return None
    
    redis.setex(key, 300, serialize(user))
    return user

方案 3:参数校验

def get_user(user_id):
    # 参数校验
    if not user_id or user_id <= 0:
        return None
    
    # 正常查询
    return query_user(user_id)

2.2 缓存击穿

问题描述

热点 key 过期

大量并发请求

全部打到数据库

数据库崩溃

解决方案

方案 1:互斥锁

def get_hot_data(key):
    data = redis.get(key)
    
    if data:
        return deserialize(data)
    
    # 获取互斥锁
    lock_key = f"lock:{key}"
    if redis.set(lock_key, "1", nx=True, ex=10):
        try:
            # 双重检查
            data = redis.get(key)
            if data:
                return deserialize(data)
            
            # 查询数据库
            data = query_db(key)
            
            # 写入缓存
            redis.setex(key, 300, serialize(data))
            
            return data
        finally:
            redis.delete(lock_key)
    else:
        # 等待后重试
        time.sleep(0.1)
        return get_hot_data(key)

方案 2:永不过期

# 热点数据不设置过期时间
redis.set("hot_key", serialize(data))

# 异步更新
def async_update():
    while True:
        data = query_db("hot_key")
        redis.set("hot_key", serialize(data))
        time.sleep(300)  # 5 分钟更新一次

方案 3:逻辑过期

def get_data_with_logic_expire(key):
    data = redis.get(key)
    
    if data:
        data_obj = deserialize(data)
        
        # 检查逻辑过期时间
        if data_obj['expire_time'] > time.time():
            return data_obj['data']
        else:
            # 异步更新
            if redis.set(f"lock:{key}", "1", nx=True, ex=10):
                threading.Thread(target=update_data, args=(key,)).start()
            
            return data_obj['data']
    
    # 首次查询
    return query_and_cache(key)

def update_data(key):
    data = query_db(key)
    cached = {
        'data': data,
        'expire_time': time.time() + 300
    }
    redis.setex(key, 3600, serialize(cached))  # 物理过期 1 小时

2.3 缓存雪崩

问题描述

大量 key 同时过期

请求全部打到数据库

数据库崩溃

原因

解决方案

方案 1:随机过期时间

def set_with_random_expire(key, data, base_expire):
    # 基础时间 + 随机时间
    expire = base_expire + random.randint(0, 300)
    redis.setex(key, expire, serialize(data))

# 使用
set_with_random_expire("user:1001", user, 300)
set_with_random_expire("user:1002", user, 300)
# 实际过期时间:300-600 秒

方案 2:多级缓存

def get_data(key):
    # L1 缓存(本地内存)
    data = local_cache.get(key)
    if data:
        return data
    
    # L2 缓存(Redis)
    data = redis.get(key)
    if data:
        local_cache.set(key, data, 60)  # 本地缓存 1 分钟
        return deserialize(data)
    
    # 数据库
    data = query_db(key)
    redis.setex(key, 300, serialize(data))
    local_cache.set(key, data, 60)
    return data

方案 3:高可用架构

Redis Cluster + Sentinel

自动故障转移

避免单点故障

三、缓存一致性

3.1 一致性方案

方案 1:先更新数据库,再删除缓存

def update_user(user_id, data):
    db.execute("UPDATE users SET ? WHERE id = ?", data, user_id)
    redis.delete(f"user:{user_id}")

问题

线程 A: 更新数据库
线程 B: 读缓存(旧数据)
线程 B: 删除缓存
线程 A: 删除缓存

结果:缓存为空,下次查询会加载旧数据

方案 2:延时双删

def update_user(user_id, data):
    # 1. 删除缓存
    redis.delete(f"user:{user_id}")
    
    # 2. 更新数据库
    db.execute("UPDATE users SET ? WHERE id = ?", data, user_id)
    
    # 3. 延迟后再次删除
    time.sleep(0.5)
    redis.delete(f"user:{user_id}")

方案 3:监听 binlog

# 使用 Canal 监听 MySQL binlog
def on_binlog_event(event):
    if event.table == 'users':
        if event.type == 'UPDATE':
            redis.delete(f"user:{event.user_id}")

方案 4:分布式事务

# 使用事务保证原子性
def update_user(user_id, data):
    with transaction():
        db.execute("UPDATE users SET ? WHERE id = ?", data, user_id)
        redis.delete(f"user:{user_id}")

3.2 一致性级别选择

场景一致性要求方案
金融交易强一致分布式事务
用户信息最终一致先 DB 后删缓存
商品库存最终一致延时双删
热点新闻弱一致异步更新

四、缓存优化

4.1 数据结构优化

# ❌ 不推荐:大 Key
user = {
    'id': 1001,
    'name': 'John',
    'profile': {...},  # 大对象
    'orders': [...]    # 大数组
}
redis.set("user:1001", json.dumps(user))

# ✅ 推荐:拆分存储
redis.hset("user:1001:base", mapping={
    'id': 1001,
    'name': 'John'
})
redis.hset("user:1001:profile", mapping=profile)
redis.lpush("user:1001:orders", *orders)

4.2 批量操作

# ❌ 不推荐:逐个操作
for user_id in user_ids:
    redis.get(f"user:{user_id}")

# ✅ 推荐:批量操作
pipe = redis.pipeline()
for user_id in user_ids:
    pipe.get(f"user:{user_id}")
results = pipe.execute()

4.3 内存优化

# 1. 使用合适的数据结构
# Hash vs String
redis.hset("user:1001", "name", "John")  # 更省内存

# 2. 设置过期时间
redis.setex("temp_key", 300, value)

# 3. 使用内存淘汰策略
# maxmemory-policy allkeys-lru

# 4. 定期清理
redis.scan_iter("temp:*")  # 扫描临时 key
redis.delete("temp:key")   # 删除

4.4 监控指标

# 关键指标
INFO stats
- keyspace_hits      # 缓存命中数
- keyspace_misses    # 缓存未命中数
- hit_rate = hits / (hits + misses)  # 命中率

INFO memory
- used_memory        # 已用内存
- maxmemory          # 最大内存
- mem_fragmentation  # 碎片率

INFO keyspace
- db0:keys=1000,expires=500  # key 数量、过期数

# 告警
if hit_rate < 0.8:
    alert("缓存命中率过低")
if used_memory / maxmemory > 0.9:
    alert("内存使用率过高")

五、最佳实践总结

5.1 设计原则

1. 只缓存热点数据
2. 设置合理的过期时间
3. 避免大 Key
4. 使用批量操作
5. 监控命中率

5.2 问题处理

问题检测解决
穿透大量 key 未命中布隆过滤器、缓存空值
击穿热点 key 过期互斥锁、永不过期
雪崩大量 key 同时过期随机过期、多级缓存
不一致缓存与 DB 数据不同延时双删、监听 binlog

5.3 配置建议

# 内存配置
maxmemory 4gb
maxmemory-policy allkeys-lru

# 持久化
save 900 1
appendonly yes

# 网络
timeout 300
tcp-keepalive 60

总结

Redis 缓存设计核心要点:

模式适用场景一致性
Cache-Aside通用场景最终一致
Read Through读多写少最终一致
Write Behind高性能场景弱一致

问题解决方案

掌握缓存设计,构建高性能系统!

参考资料


分享这篇文章到:

上一篇文章
Kafka Schema Registry 数据格式管理实战
下一篇文章
重读《活着》:苦难中的生命韧性