分布式缓存

分布式缓存是提升系统性能的核心手段。本节深入讲解Redis缓存架构、集群方案、缓存策略及常见问题解决方案。

1. 缓存架构设计原则

1.1 缓存适用场景

场景特征缓存效果
高并发读读多写少,热数据集中降低DB压力90%+
热点数据符合二八定律显著提升响应速度
低延迟需求毫秒级响应要求绕过DB直连缓存

1.2 多级缓存架构

┌─────────────────────────────────────────────────────┐
│                     应用服务器                       │
│  ┌─────────────┐                                   │
│  │ L1本地缓存   │ ◀── 微秒级访问                    │
│  │ (Caffeine)  │                                   │
│  └──────┬──────┘                                   │
│         │ 未命中                                    │
│  ┌──────▼──────┐                                   │
│  │ L2 Redis    │ ◀── 毫秒级访问                     │
│  │   Cluster   │                                   │
│  └──────┬──────┘                                   │
│         │ 未命中                                    │
│  ┌──────▼──────┐                                   │
│  │   数据库     │                                   │
│  └─────────────┘                                   │
└─────────────────────────────────────────────────────┘

设计要点

  • L1缓存:存储热点数据,容量小(MB级),响应极快
  • L2缓存:存储更大量数据,容量大(GB级),共享访问
  • 读取策略:L1 → L2 → DB

2. 缓存策略

2.1 Cache-Aside(旁路缓存)

最常用的缓存模式

def get_user(user_id):
    # 1. 先查缓存
    user = redis.get(f"user:{user_id}")
    if user:
        return json.loads(user)
    
    # 2. 缓存未命中,查数据库
    user = db.query("SELECT * FROM users WHERE id = ?", user_id)
    
    # 3. 写入缓存
    if user:
        redis.setex(f"user:{user_id}", 3600, json.dumps(user))
    
    return user
 
def update_user(user_id, data):
    # 1. 先更新数据库
    db.execute("UPDATE users SET ... WHERE id = ?", user_id, data)
    
    # 2. 删除缓存(而非更新)
    redis.delete(f"user:{user_id}")

为什么删除而非更新?

  • 避免并发时的脏数据问题
  • 更新缓存时如果线程B也更新了数据库,线程A再更新缓存会导致数据不一致

2.2 缓存更新策略对比

策略写操作读操作一致性性能适用场景
Cache-Aside删除缓存先缓存后DB最终一致读多写少
Write-Through同步写缓存和DB直接读缓存强一致写多读少
Write-Behind先写缓存异步写DB直接读缓存最终一致最高高并发写入
Read-Through缓存代理自动加载缓存未命中自动查DB简化代码逻辑

2.3 缓存粒度控制

# 粒度太粗:缓存整个用户对象
redis.setex(f"user:{user_id}", 3600, json.dumps(user_object))
 
# 粒度适中:只缓存需要的字段
redis.hset(f"user:{user_id}", mapping={
    "name": user.name,
    "email": user.email
})
redis.expire(f"user:{user_id}", 3600)
 
# 粒度设计原则
# 1. 最小可用原则:只缓存业务需要的字段
# 2. 读写匹配原则:缓存粒度与业务读写频率匹配
# 3. 避免大对象:单个Key不宜过大

3. Redis集群方案

3.1 主从复制

# 配置主从复制
replicaof master_ip 6379
 
# 或者在redis.conf中配置
replicaof <masterip> <masterport>
replica-read-only yes

读写分离策略

  • 主节点:写操作
  • 从节点:读操作(需注意复制延迟)

3.2 Redis Sentinel(哨兵模式)

# 哨兵配置
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel parallel-syncs mymaster 1

哨兵职责

  • 监控主从节点健康状态
  • 自动故障转移(主节点宕机时将从节点升级)
  • 通知客户端新的主节点地址

3.3 Redis Cluster

┌─────────────────────────────────────────────────────┐
│                   Redis Cluster                      │
│                                                      │
│   槽位范围: 0-5460      槽位范围: 5461-10922        │
│   ┌─────────┐         ┌─────────┐                  │
│   │ Master1 │────────│ Master2 │                  │
│   └────┬────┘         └────┬────┘                  │
│        │                    │                       │
│   ┌────▼────┐         ┌────▼────┐                  │
│   │ Replica1│         │ Replica2│                  │
│   └─────────┘         └─────────┘                  │
│                                                      │
│   槽位范围: 10923-16383                              │
│   ┌─────────┐                                       │
│   │ Master3 │                                       │
│   └────┬────┘                                       │
│   ┌────▼────┐                                       │
│   │ Replica3│                                       │
│   └─────────┘                                       │
└─────────────────────────────────────────────────────┘

特点

  • 数据分片:16384个槽位,自动分配到不同节点
  • 水平扩展:增加节点即可扩容
  • 高可用:主从自动故障转移
# 创建集群
redis-cli --cluster create 192.168.1.10:7000 192.168.1.10:7001 \
  192.168.1.11:7002 192.168.1.11:7003 \
  192.168.1.12:7004 192.168.1.12:7005 \
  --cluster-replicas 1
 
# 查看集群状态
redis-cli --cluster check 192.168.1.10:7000

4. 缓存三大问题

4.1 缓存穿透

定义:查询不存在的数据,绕过缓存直接打穿数据库。

请求 → Redis(未命中) → DB(未命中) → 返回空
              ↑
         大量无效请求穿透

解决方案

方案1:布隆过滤器

from bloom_filter import BloomFilter
 
bf = BloomFilter(capacity=1000000, error_rate=0.01)
 
# 添加存在的Key
bf.add("user:1")
bf.add("user:2")
 
# 查询
if "user:123" in bf:
    # 可能存在,继续查Redis/DB
    pass
else:
    # 一定不存在,直接返回
    return None

方案2:缓存空值

def get_user(user_id):
    user = redis.get(f"user:{user_id}")
    if user is None:
        user = db.query("SELECT * FROM users WHERE id = ?", user_id)
        if user:
            redis.setex(f"user:{user_id}", 3600, json.dumps(user))
        else:
            # 缓存空值,短过期时间
            redis.setex(f"user:{user_id}", 60, "NULL")
    elif user == "NULL":
        return None
    return user

4.2 缓存击穿

定义:热点Key过期瞬间,大量请求同时击穿到数据库。

热点Key: "product:hot:1001" (已过期)
         ↓
    大量请求同时查Redis (未命中)
         ↓
    同时打向数据库 → 数据库崩溃

解决方案

方案1:互斥锁

import redis
import uuid
 
lock_key = f"lock:user:{user_id}"
lock_value = str(uuid.uuid4())
 
# 尝试获取锁
if redis.set(lock_key, lock_value, nx=True, ex=10):
    try:
        # 成功获取锁,查询数据库
        user = db.query(...)
        redis.setex(f"user:{user_id}", 3600, json.dumps(user))
    finally:
        # 释放锁
        if redis.get(lock_key) == lock_value:
            redis.delete(lock_key)
else:
    # 获取锁失败,短暂等待后重试
    time.sleep(0.1)
    return redis.get(f"user:{user_id}")

方案2:逻辑过期

def get_user(user_id):
    user_json = redis.get(f"user:{user_id}")
    if not user_json:
        return None
    
    user = json.loads(user_json)
    
    # 检查逻辑过期时间
    if user['logic_expire_time'] < time.time():
        # 已过期,但返回旧数据
        # 启动异步线程更新缓存
        asyncio.create_task(refresh_cache(user_id))
        return user
    
    return user

4.3 缓存雪崩

定义:大量Key同时过期或Redis宕机,导致数据库压力骤增。

解决方案

方案1:过期时间随机化

# 设置过期时间时加上随机偏移
base_timeout = 3600
random_offset = random.randint(0, 300)
redis.setex(key, base_timeout + random_offset, value)

方案2:多级缓存

# 本地缓存作为Redis的备份
local_cache = CaffeineCache(max_size=10000)
 
def get_user(user_id):
    # 先查本地缓存
    user = local_cache.get(user_id)
    if user:
        return user
    
    # 再查Redis
    user = redis.get(f"user:{user_id}")
    if user:
        local_cache.set(user_id, user)
        return user
    
    # 最后查数据库
    user = db.query(...)
    if user:
        redis.setex(f"user:{user_id}", 3600, json.dumps(user))
        local_cache.set(user_id, user)
    return user

方案3:限流降级

from sentinel import CircuitBreaker
 
breaker = CircuitBreaker(failure_threshold=5, recovery_timeout=30)
 
@breaker
def get_user_circuit(user_id):
    # 当Redis/DB错误率过高时,返回降级数据
    return get_user_fallback(user_id)

5. Redis 内存优化

5.1 内存淘汰策略

当Redis内存达到上限时,根据策略删除Key。

策略作用范围算法适用场景
noeviction不淘汰,返回错误数据不能丢失
allkeys-lru所有KeyLRU通用缓存
allkeys-lfu所有KeyLFU有明显热点
allkeys-random所有Key随机无规律访问
volatile-lru有TTL的KeyLRU缓存+持久混合
volatile-lfu有TTL的KeyLFUTTL数据有热点
volatile-random有TTL的Key随机-
volatile-ttl有TTL的KeyTTL最小优先删除快过期
# 配置
maxmemory 4gb
maxmemory-policy allkeys-lfu

5.2 Big Key处理

# 查找Big Key
redis-cli --bigkeys
 
# 扫描大Key
redis-cli --scan --pattern '*' | while read key; do
    size=$(redis-cli memory usage "$key")
    if [ $size -gt 10485760 ]; then  # > 10MB
        echo "$key: $size bytes"
    fi
done

5.3 内存碎片优化

# 内存碎片率
redis-cli info memory | grep mem_fragmentation_ratio
 
# 碎片率 > 1.5 时需要优化
# 方案1:重启Redis
# 方案2:设置activedefrag yes

6. Redis持久化

6.1 RDB(快照)

# 配置
save 3600 1      # 1小时内有1次写入
save 300 100     # 5分钟内有100次写入
save 60 10000    # 1分钟内有10000次写入
 
# 手动触发
redis-cli save  # 同步(阻塞)
redis-cli bgsave  # 异步(非阻塞)

优点:恢复速度快
缺点:可能丢失最后一次快照后的数据

6.2 AOF(追加日志)

# 配置
appendonly yes
appendfsync everysec  # 每秒刷盘,性能与安全性平衡
# appendfsync always  # 每次写都刷盘,最安全但最慢
# appendfsync no       # 由OS决定何时刷盘

优点:数据安全性更高
缺点:文件较大,恢复较慢

6.3 混合持久化(Redis 4.0+)

# 开启混合持久化
aof-use-rdb-preamble yes

原理:AOF重写时使用RDB格式开头,之后用AOF格式追加


7. 高可用架构实践

7.1 生产环境Redis架构

                    ┌─────────────────┐
                    │   负载均衡器     │
                    └────────┬────────┘
                             │
        ┌────────────────────┼────────────────────┐
        │                    │                    │
   ┌────▼─────┐        ┌────▼─────┐        ┌────▼─────┐
   │ 应用节点1 │        │ 应用节点2 │        │ 应用节点3 │
   └────┬─────┘        └────┬─────┘        └────┬─────┘
        │                   │                   │
        └───────────────────┼───────────────────┘
                            │
              ┌─────────────┼─────────────┐
              │             │             │
         ┌────▼────┐   ┌────▼────┐   ┌────▼────┐
         │ Master1 │   │ Master2 │   │ Master3 │
         └────┬────┘   └────┬────┘   └────┬────┘
              │             │             │
         ┌────▼────┐   ┌────▼────┐   ┌────▼────┐
         │ Replica1│   │ Replica2│   │ Replica3│
         └─────────┘   └─────────┘   └─────────┘
              │             │             │
         ┌────▼────┐   ┌────▼────┐   ┌────▼────┐
         │ Sentinel│   │ Sentinel│   │ Sentinel│
         └─────────┘   └─────────┘   └─────────┘

7.2 监控指标

指标含义健康阈值
used_memoryRedis已分配内存< maxmemory的90%
keyspace_hits缓存命中次数命中率 > 80%
keyspace_misses缓存未命中次数-
connected_clients客户端连接数< maxclients的80%
mem_fragmentation_ratio内存碎片率< 1.5
replication_lag主从复制延迟< 1秒
# 查看关键指标
redis-cli info stats | grep -E "keyspace|cmdstat"
redis-cli info memory | grep -E "used|peak|maxmemory"

8. 参考资料


相关主题