Day67 - Redis 高可用与缓存问题
本部分涵盖 Redis 高可用架构(主从、哨兵、集群)和三大缓存问题(穿透、击穿、雪崩)。
1. Redis 主从复制
1.1 主从复制原理
sequenceDiagram
participant Master as 主库
participant Slave as 从库
Master->>Master: 接收写命令
Master->>Master: 执行命令
Master->>Master: 记录命令到命令缓冲区
Note over Master,Slave: 增量同步
Master->>Slave: 发送 RDB 文件
Master->>Slave: 发送缓冲区命令
Slave->>Slave: 加载 RDB
Slave->>Slave: 执行增量命令
1.2 配置主从
# 方式1:命令行配置
redis-cli slaveof 192.168.1.100 6379
# 取消主从
redis-cli slaveof no one
# 方式2:配置文件
replicaof 192.168.1.100 6379
replica-read-only yes
1.3 主从复制流程
-- 完整重同步流程
1. slave 发送 PSYNC 命令给 master
2. master 判断是否首次同步
3. master 执行 BGSAVE 生成 RDB
4. master 发送 RDB 给 slave
5. slave 加载 RDB 数据
6. master 发送缓冲区命令
7. 完成同步
-- 增量同步
1. master 记录写命令到 replication buffer
2. master 发送 PING + 命令给 slave
3. slave 执行命令
2. 哨兵模式
2.1 哨兵架构
据《Redis哨兵模式和分片集群》(CSDN),哨兵模式通过监控主从节点状态实现自动故障转移。
graph TB
subgraph 应用层
App["客户端"]
end
subgraph 哨兵集群
S1["哨兵1"]
S2["哨兵2"]
S3["哨兵3"]
end
subgraph Redis集群
Master["主库<br/>Master"]
Slave1["从库1<br/>Slave"]
Slave2["从库2<br/>Slave"]
end
App <--> S1
App <--> S2
App <--> S3
S1 --> Master
S1 --> Slave1
S1 --> Slave2
S2 --> Master
S2 --> Slave1
S2 --> Slave2
S3 --> Master
S3 --> Slave1
S3 --> Slave2
Master --> Slave1
Master --> Slave2
2.2 哨兵核心功能
| 功能 | 说明 |
|---|---|
| 监控 | 定期检测主从节点存活状态 |
| 通知 | 故障发生时通知客户端 |
| 自动故障转移 | 自动将从库升级为主库 |
| 配置提供者 | 提供当前主库地址 |
2.3 主观下线与客观下线
| 概念 | 说明 |
|---|---|
| SDOWN(主观下线) | 单个哨兵认为主节点不可达 |
| ODOWN(客观下线) | 足够多哨兵认为主节点不可达 |
# sentinel.conf 配置
sentinel monitor mymaster 192.168.1.100 6379 2
# 集群名 主库IP 端口 法定票数
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 30000
2.4 故障转移流程
graph TB
subgraph 故障转移
A["主库不可达"] --> B["多个哨兵确认ODOWN"]
B --> C["投票选举领头哨兵"]
C --> D["选择最优从库"]
D --> E["从库升级为主库"]
E --> F["其他从库切换主库"]
F --> G["通知客户端新主库"]
end
2.5 选主规则
-- 优先级选择
1. replica-priority 越大越优先
2. 复制偏移量越大(数据越新)
3. run ID 越小(启动越早)
3. Redis Cluster 集群
3.1 集群架构
据《Redis cluster specification》(Redis 官方文档),Redis Cluster 将数据分为 16384 个哈希槽,实现数据分片。
graph TB
subgraph 集群节点
N1["节点1<br/>Master:0-5460"]
N2["节点2<br/>Master:5461-10922"]
N3["节点3<br/>Master:10923-16383"]
N4["节点4<br/>Slave1"]
N5["节点5<br/>Slave2"]
N6["节点6<br/>Slave3"]
end
N1 <--> N2
N1 <--> N3
N2 <--> N3
N1 --- N4
N2 --- N5
N3 --- N6
3.2 哈希槽原理
# 键计算槽位
slot = CRC16(key) % 16384
# 示例
key = "user:1001"
CRC16("user:1001") = 12345
slot = 12345 % 16384 = 12345
# 分配槽位
redis-cli cluster addslots 0 1 2 ... 5460
3.3 集群配置
# 节点配置
cluster-enabled yes
cluster-node-timeout 15000
cluster-config-file nodes.conf
# 集群管理命令
redis-cli --cluster create 192.168.1.101:6379 192.168.1.102:6379 192.168.1.103:6379 \
--cluster-replicas 1
3.4 集群 vs 哨兵
| 特性 | 哨兵模式 | 集群模式 |
|---|---|---|
| 数据分片 | 不支持 | 支持(16384槽) |
| 写能力 | 单主库 | 多主库 |
| 扩容 | 纵向 | 横向 |
| 复杂度 | 低 | 高 |
| 适用场景 | 中小规模 | 大规模高并发 |
4. 缓存穿透
4.1 问题描述
据《Redis 缓存三大核心问题:缓存穿透/击穿/雪崩》(阿里云开发者社区),缓存穿透指查询一个不存在的数据,导致请求直达数据库。
graph LR
R["请求"] --> C["缓存"]
C -->|"miss"| D["数据库"]
D -->|"无数据"| R
R -->|"重复请求"| D
4.2 解决方案
-- 方案1:布隆过滤器
-- 在缓存层前加布隆过滤器,快速判断数据是否存在
-- 方案2:空值缓存
-- 将空结果也缓存,设置较短过期时间
SET product:9999 "" EX 60
-- 方案3:参数校验
-- 业务层拦截非法参数
IF id <= 0 THEN RETURN null
# Python 示例
def get_product(product_id):
# 1. 参数校验
if product_id <= 0:
return None
# 2. 布隆过滤器判断
if not bloom_filter.contains(product_id):
return None # 一定不存在
# 3. 查缓存
product = redis.get(f"product:{product_id}")
if product:
return product
# 4. 查数据库
product = db.query("SELECT * FROM products WHERE id=?", product_id)
# 5. 缓存结果
if product:
redis.setex(f"product:{product_id}", 3600, json.dumps(product))
else:
# 缓存空值,避免穿透
redis.setex(f"product:{product_id}", 60, "")
return product
5. 缓存击穿
5.1 问题描述
缓存击穿指热点 Key 过期的瞬间,大量请求涌入数据库。
graph LR
R["请求"] --> C["缓存热点key<br/>过期瞬间"]
C -->|"击穿"| D["数据库"]
D --> R
5.2 解决方案
-- 方案1:互斥锁
-- 只允许一个线程重建缓存
-- 方案2:热点数据永不过期
-- 通过逻辑过期代替物理过期
-- 方案3:过期时间随机化
SET hot_key value EX 3600
PEXPIREAT hot_key timestamp + random(0, 600)
# 互斥锁实现
import redis
import json
import time
lock_key = f"lock:product:{product_id}"
cache_key = f"product:{product_id}"
# 尝试获取锁
lock = redis.set(lock_key, "1", nx=True, ex=10)
if lock:
try:
# 查数据库
product = db.query("SELECT * FROM products WHERE id=?", product_id)
# 更新缓存
redis.setex(cache_key, 3600, json.dumps(product))
finally:
redis.delete(lock_key)
else:
# 等待后重试
time.sleep(0.1)
return redis.get(cache_key)
6. 缓存雪崩
6.1 问题描述
大量缓存同时过期,导致大量请求直接访问数据库。
graph LR
K1["key1过期"] & K2["key2过期"] & K3["key3过期"] --> D["数据库崩溃"]
6.2 解决方案
-- 方案1:过期时间随机化
SET key1 value EX 3600
SET key2 value EX 3700
SET key3 value EX 3500
-- 方案2:多级缓存
-- L1: 本地缓存 1分钟
-- L2: Redis缓存 1小时
-- 方案3:服务降级
-- 限流、熔断保护数据库
# 多级缓存实现
class CacheService:
def get_product(self, product_id):
local_key = f"local:product:{product_id}"
# 1. 查本地缓存
product = local_cache.get(local_key)
if product:
return product
# 2. 查Redis缓存
redis_key = f"redis:product:{product_id}"
product = redis.get(redis_key)
if product:
# 回填本地缓存
local_cache.setex(local_key, 60, product)
return product
# 3. 查数据库
product = db.query(product_id)
# 4. 回填缓存
redis.setex(redis_key, 3600, product)
local_cache.setex(local_key, 60, product)
return product
7. 缓存问题对比总结
| 问题 | 原因 | 核心方案 |
|---|---|---|
| 缓存穿透 | 查询不存在的数据 | 布隆过滤器、空值缓存 |
| 缓存击穿 | 热点 Key 过期 | 互斥锁、逻辑过期 |
| 缓存雪崩 | 大量 Key 同时过期 | 随机过期、多级缓存、限流熔断 |
8. Redis 内存管理
8.1 内存淘汰策略
# 配置
maxmemory 2gb
maxmemory-policy allkeys-lru
# 策略说明
noeviction # 不淘汰(默认)
allkeys-lru # 所有key LRU淘汰
allkeys-random # 所有key随机淘汰
volatile-lru # 已过期key LRU淘汰
volatile-random # 已过期key随机淘汰
volatile-ttl # 已过期key TTL淘汰
8.2 内存优化
# 合理使用数据结构
HSET user:1 name "zhang" age "25" # Hash 优于 String
# 压缩列表优化
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
# 过期键处理
# 惰性删除:访问时检查并删除
# 定期删除:每隔一段时间检查