Redis 部署架构
概述
Redis 提供了多种部署架构来满足不同的业务需求,从简单的主从复制到复杂的分布式集群。本文将详细介绍 Redis 的三种主要部署架构:主从复制、哨兵模式和集群模式。
1. Redis 主从复制
1.1 架构原理
Redis 主从复制是一种数据冗余和读写分离的解决方案。在主从架构中,有一个主节点(Master)和一个或多个从节点(Slave/Replica)。
graph TD
A[Master 主节点] --> B[Slave1 从节点]
A --> C[Slave2 从节点]
A --> D[Slave3 从节点]
E[Client 写请求] --> A
F[Client 读请求] --> B
G[Client 读请求] --> C
H[Client 读请求] --> D
1.2 复制机制
1.2.1 全量复制(Full Resynchronization)
当从节点首次连接主节点或重新连接时,会进行全量复制:
- 建立连接:从节点向主节点发送
PSYNC命令 - 生成快照:主节点执行
BGSAVE生成 RDB 文件 - 传输数据:主节点将 RDB 文件发送给从节点
- 加载数据:从节点清空现有数据,加载 RDB 文件
- 同步增量:传输期间的写命令通过复制缓冲区同步
1.2.2 增量复制(Partial Resynchronization)
Redis 2.8+ 引入了增量复制机制:
- 复制偏移量:主从节点维护复制偏移量
- 复制积压缓冲区:主节点维护固定大小的环形缓冲区
- 服务器运行ID:每个 Redis 实例的唯一标识
# 主节点配置
bind 0.0.0.0
port 6379
# 从节点配置
bind 0.0.0.0
port 6380
replicaof 127.0.0.1 6379
# 或使用命令:SLAVEOF 127.0.0.1 6379
1.3 复制流程详解
sequenceDiagram
participant S as Slave
participant M as Master
S->>M: PSYNC runid offset
alt 首次连接或runid不匹配
M->>S: +FULLRESYNC runid offset
M->>M: BGSAVE 生成RDB
M->>S: 发送RDB文件
M->>S: 发送缓冲区命令
else 增量复制
M->>S: +CONTINUE
M->>S: 发送缺失的命令
end
loop 持续同步
M->>S: 实时命令同步
end
1.4 优点
- 读写分离:主节点处理写操作,从节点处理读操作
- 数据冗余:提供数据备份,提高可用性
- 负载均衡:分散读请求压力
- 简单易用:配置简单,维护成本低
1.5 缺点
- 单点故障:主节点故障时需要手动切换
- 数据一致性:异步复制可能导致数据丢失
- 复制延迟:网络延迟影响数据同步
- 写操作瓶颈:所有写操作集中在主节点
1.6 配置示例
# 主节点 redis-master.conf
port 6379
bind 0.0.0.0
requirepass "master_password"
masterauth "master_password"
# 从节点 redis-slave.conf
port 6380
bind 0.0.0.0
replicaof 127.0.0.1 6379
masterauth "master_password"
requirepass "slave_password"
replica-read-only yes
2. Redis 哨兵模式(Sentinel)
2.1 解决的问题
哨兵模式主要解决主从复制架构中的以下问题:
- 主节点故障检测:自动发现主节点故障
- 自动故障转移:自动将从节点提升为主节点
- 配置管理:动态更新客户端连接信息
- 通知机制:故障转移时通知管理员和应用程序
2.2 架构设计
graph TD
subgraph "Sentinel 集群"
S1[Sentinel 1]
S2[Sentinel 2]
S3[Sentinel 3]
end
subgraph "Redis 集群"
M[Master]
R1[Replica 1]
R2[Replica 2]
end
S1 -.-> M
S1 -.-> R1
S1 -.-> R2
S2 -.-> M
S2 -.-> R1
S2 -.-> R2
S3 -.-> M
S3 -.-> R1
S3 -.-> R2
M --> R1
M --> R2
C[Client] --> S1
C --> S2
C --> S3
2.3 工作机制
2.3.1 监控(Monitoring)
- 哨兵定期向主从节点发送
PING命令 - 检查节点的响应时间和状态
- 维护节点的在线状态信息
2.3.2 故障检测
主观下线(Subjective Down, SDOWN):
- 单个哨兵认为节点不可达
- 超过
down-after-milliseconds时间未响应
客观下线(Objective Down, ODOWN):
- 多个哨兵确认节点不可达
- 达到
quorum数量的哨兵同意
2.3.3 故障转移流程
sequenceDiagram
participant S1 as Sentinel 1
participant S2 as Sentinel 2
participant S3 as Sentinel 3
participant M as Master
participant R1 as Replica 1
participant R2 as Replica 2
S1->>M: PING (超时)
S1->>S1: 标记主观下线
S1->>S2: 询问Master状态
S1->>S3: 询问Master状态
S2->>S1: 确认下线
S3->>S1: 确认下线
S1->>S1: 标记客观下线
S1->>S2: 请求成为Leader
S1->>S3: 请求成为Leader
S2->>S1: 投票同意
S3->>S1: 投票同意
S1->>R1: 选择新Master
S1->>R1: SLAVEOF NO ONE
S1->>R2: SLAVEOF new_master
S1->>S2: 通知配置更新
S1->>S3: 通知配置更新
2.4 配置示例
2.4.1 哨兵配置文件
# sentinel.conf
port 26379
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel auth-pass mymaster your_password
sentinel down-after-milliseconds mymaster 5000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 10000
# 通知脚本
sentinel notification-script mymaster /opt/notify.sh
sentinel client-reconfig-script mymaster /opt/reconfig.sh
2.4.2 启动命令
# 启动哨兵
redis-sentinel /path/to/sentinel.conf
# 或
redis-server /path/to/sentinel.conf --sentinel
2.5 优点
- 高可用性:自动故障检测和转移
- 无单点故障:哨兵集群保证可靠性
- 自动化运维:减少人工干预
- 配置管理:动态服务发现
2.6 缺点
- 复杂性增加:需要额外的哨兵节点
- 脑裂风险:网络分区可能导致多个主节点
- 资源消耗:额外的监控开销
- 写操作限制:仍然只有一个主节点处理写操作
3. Redis 集群模式(Cluster)
3.1 分布式架构
Redis 集群是一个分布式的 Redis 实现,提供了数据分片和高可用性。
graph TD
subgraph "Redis Cluster"
subgraph "Node 1 (0-5460)"
M1[Master 1]
S1[Slave 1]
end
subgraph "Node 2 (5461-10922)"
M2[Master 2]
S2[Slave 2]
end
subgraph "Node 3 (10923-16383)"
M3[Master 3]
S3[Slave 3]
end
end
M1 --> S1
M2 --> S2
M3 --> S3
M1 -.-> M2
M2 -.-> M3
M3 -.-> M1
C[Client] --> M1
C --> M2
C --> M3
3.2 Slot 机制演进
3.2.1 分布式哈希算法演进
在分布式系统中,数据分片算法经历了从简单到复杂的演进过程:
1. 普通取模算法
最简单的分片方式是对节点数量取模:
# 普通取模
node = hash(key) % N # N为节点数量
# 示例:3个节点
hash("user:1001") % 3 = 1 # 存储在节点1
hash("user:1002") % 3 = 2 # 存储在节点2
hash("user:1003") % 3 = 0 # 存储在节点0
普通取模的问题:
- 节点数量变化时,大量数据需要重新分布
- 扩容或缩容成本极高
- 数据迁移量巨大
2. 一致性哈希算法
为解决普通取模的问题,引入了一致性哈希:
graph TD
subgraph "一致性哈希环"
A[Node A<br/>Hash: 100]
B[Node B<br/>Hash: 200]
C[Node C<br/>Hash: 300]
K1[Key1<br/>Hash: 150]
K2[Key2<br/>Hash: 250]
K3[Key3<br/>Hash: 50]
end
K1 --> B
K2 --> C
K3 --> A
style A fill:#ffcccc
style B fill:#ccffcc
style C fill:#ccccff
一致性哈希特点:
- 节点和数据都映射到哈希环上
- 数据存储在顺时针方向第一个节点
- 节点变化时只影响相邻节点的数据
- 引入虚拟节点解决数据倾斜问题
一致性哈希的局限:
- 实现复杂度较高
- 虚拟节点管理复杂
- 数据分布仍可能不均匀
- 节点故障时数据重分布逻辑复杂
3. Redis 哈希槽方案
Redis 采用了更简单有效的哈希槽方案:
graph TD
subgraph "哈希槽分配"
S1["Slots 0-5460<br/>(5461个槽)"]
S2["Slots 5461-10922<br/>(5462个槽)"]
S3["Slots 10923-16383<br/>(5461个槽)"]
end
subgraph "节点映射"
N1[Node 1]
N2[Node 2]
N3[Node 3]
end
S1 --> N1
S2 --> N2
S3 --> N3
K["Key: user:1001<br/>CRC16: 9842<br/>Slot: 9842"] --> S2
3.2.2 哈希槽分配机制
Redis 集群将整个数据空间分为 16384 个哈希槽(slot):
# 槽位计算公式
slot = CRC16(key) % 16384
# 示例
CRC16("user:1001") % 16384 = 9842
# 该键存储在负责 9842 槽位的节点上
哈希槽的优势:
- 固定槽位数量:16384个槽位数量固定,不随节点变化
- 简单映射:槽位到节点的映射关系清晰
- 灵活迁移:可以精确控制数据迁移粒度
- 负载均衡:可以根据需要调整槽位分配
3.2.3 为什么是 16384 个槽位?
Redis 选择 16384 个槽位的设计考虑:
1. 心跳包大小限制
Redis 集群节点间通过 Gossip 协议进行通信,每个心跳包都需要携带槽位信息:
# 槽位信息在心跳包中的表示
# 使用位图(bitmap)表示槽位分配状态
# 16384 slots = 16384 bits = 2048 bytes = 2KB
# 如果使用 65536 个槽位:
# 65536 slots = 65536 bits = 8192 bytes = 8KB
官方设计考虑(来源:Redis 官方文档):
"The reason is that normal heartbeat packets carry the full configuration of a node, that can be replaced in an idempotent way with the old in order to update an old config. This means they contain the slots configuration for a node, in raw form, that uses 2k of space with16384 slots, but would use a prohibitive 8k of space using 65536 slots."
2. 网络带宽优化
sequenceDiagram
participant N1 as Node 1
participant N2 as Node 2
participant N3 as Node 3
Note over N1,N3: 心跳包结构
N1->>N2: PING<br/>包含:节点信息+槽位bitmap(2KB)
N2->>N1: PONG<br/>包含:节点信息+槽位bitmap(2KB)
N1->>N3: PING<br/>包含:节点信息+槽位bitmap(2KB)
N3->>N1: PONG<br/>包含:节点信息+槽位bitmap(2KB)
Note over N1,N3: 每秒多次心跳,2KB vs 8KB差异巨大
3. 集群规模考虑
- 理论最大节点数:16384 个(每个节点至少负责1个槽位)
- 实际推荐节点数:1000 个以内
- 槽位分配粒度:平均每个节点 16-32 个槽位(500-1000节点场景)
4. 内存使用优化
// Redis 源码中的槽位表示
// 每个节点维护的槽位信息
typedef struct clusterNode {
// ...
unsigned char slots[CLUSTER_SLOTS/8]; // 16384/8 = 2048 bytes
// ...
} clusterNode;
3.2.4 心跳机制与槽位信息传播
心跳包结构:
// Redis 集群心跳包结构(简化版)
typedef struct {
char sig[4]; // 签名 "RCmb"
uint32_t totlen; // 总长度
uint16_t ver; // 协议版本
uint16_t port; // 端口
uint16_t type; // 消息类型 (PING/PONG/MEET/FAIL)
uint16_t count; // 节点数量
uint64_t currentEpoch; // 当前纪元
uint64_t configEpoch; // 配置纪元
char sender[CLUSTER_NAMELEN]; // 发送者ID
unsigned char myslots[CLUSTER_SLOTS/8]; // 槽位bitmap
// ... 其他节点信息
} clusterMsg;
槽位信息传播流程:
sequenceDiagram
participant N1 as Node 1<br/>(Slots: 0-5460)
participant N2 as Node 2<br/>(Slots: 5461-10922)
participant N3 as Node 3<br/>(Slots: 10923-16383)
Note over N1,N3: 定期心跳(每秒1次)
N1->>N2: PING<br/>myslots[0-5460]=1, others=0
N2->>N1: PONG<br/>myslots[5461-10922]=1, others=0
N1->>N3: PING<br/>myslots[0-5460]=1, others=0
N3->>N1: PONG<br/>myslots[10923-16383]=1, others=0
N2->>N3: PING<br/>myslots[5461-10922]=1, others=0
N3->>N2: PONG<br/>myslots[10923-16383]=1, others=0
Note over N1,N3: 每个节点都知道全局槽位分配
心跳频率与网络开销:
# 心跳配置参数
cluster-node-timeout 15000 # 节点超时时间(ms)
# 心跳频率计算
# ping_interval = min(node_timeout/2, 1000ms)
# 默认每秒1次心跳
# 网络开销计算(100个节点的集群):
# 每个节点每秒发送心跳:99次(向其他所有节点)
# 每个心跳包大小:约2KB(槽位信息)+ 节点信息
# 单节点每秒网络开销:99 * 2KB = 198KB
# 集群总网络开销:100 * 198KB = 19.8MB/s
3.2.5 槽位分配策略
# 3个主节点的槽位分配
Node 1: 0-5460 (5461 slots)
Node 2: 5461-10922 (5462 slots)
Node 3: 10923-16383 (5461 slots)
3.3 集群拓扑
3.3.1 节点发现
sequenceDiagram
participant N1 as Node 1
participant N2 as Node 2
participant N3 as Node 3
participant N4 as Node 4
N1->>N2: MEET 命令
N2->>N1: 握手确认
N1->>N3: 传播节点信息
N2->>N4: 传播节点信息
loop Gossip 协议
N1->>N2: PING (节点状态)
N2->>N3: PING (节点状态)
N3->>N4: PING (节点状态)
N4->>N1: PING (节点状态)
end
3.3.2 Gossip 协议
每个节点定期向其他节点发送 Gossip 消息,包含:
- 节点状态信息
- 槽位分配信息
- 故障检测信息
- 配置更新信息
3.4 故障检测与转移
3.4.1 故障检测
# 节点状态
- PFAIL: 疑似故障(主观判断)
- FAIL: 确认故障(客观判断)
- HANDSHAKE: 握手状态
- NOADDR: 无地址状态
3.4.2 自动故障转移
sequenceDiagram
participant M as Master
participant S1 as Slave 1
participant S2 as Slave 2
participant N1 as Other Node 1
participant N2 as Other Node 2
M->>X: 节点故障
N1->>N1: 检测到Master故障
N2->>N2: 检测到Master故障
N1->>S1: 通知故障
N2->>S1: 通知故障
S1->>S1: 发起选举
S1->>N1: 请求投票
S1->>N2: 请求投票
N1->>S1: 投票确认
N2->>S1: 投票确认
S1->>S1: 提升为Master
S1->>N1: 广播新配置
S1->>N2: 广播新配置
3.5 集群配置
3.5.1 节点配置
# redis-cluster.conf
port 7000
cluster-enabled yes
cluster-config-file nodes-7000.conf
cluster-node-timeout 5000
appendonly yes
# 启动节点
redis-server redis-cluster.conf
3.5.2 创建集群
# Redis 5.0+ 使用 redis-cli
redis-cli --cluster create \
127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 \
127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \
--cluster-replicas 1
# 检查集群状态
redis-cli --cluster check 127.0.0.1:7000
# 查看集群信息
redis-cli -c -p 7000
127.0.0.1:7000> CLUSTER INFO
127.0.0.1:7000> CLUSTER NODES
3.6 数据迁移
3.6.1 槽位迁移
# 迁移槽位 1000 从节点 A 到节点 B
redis-cli --cluster reshard 127.0.0.1:7000
# 手动迁移步骤
# 1. 在目标节点导入槽位
CLUSTER SETSLOT 1000 IMPORTING source_node_id
# 2. 在源节点导出槽位
CLUSTER SETSLOT 1000 MIGRATING target_node_id
# 3. 迁移键值
MIGRATE target_host target_port "" 0 5000 KEYS key1 key2 ...
# 4. 确认迁移完成
CLUSTER SETSLOT 1000 NODE target_node_id
3.7 优点
- 水平扩展:支持动态添加/删除节点
- 高可用性:自动故障检测和转移
- 数据分片:自动数据分布和负载均衡
- 无单点故障:去中心化架构
- 线性扩展:性能随节点数量线性增长
3.8 缺点
- 复杂性:运维和故障排查复杂
- 事务限制:跨槽位事务不支持
- 客户端复杂:需要集群感知的客户端
- 数据倾斜:可能出现热点数据问题
- 网络开销:节点间通信开销
4. 架构选择指南
4.1 场景对比
| 特性 | 主从复制 | 哨兵模式 | 集群模式 |
|---|---|---|---|
| 高可用性 | ❌ | ✅ | ✅ |
| 自动故障转移 | ❌ | ✅ | ✅ |
| 水平扩展 | ❌ | ❌ | ✅ |
| 运维复杂度 | 低 | 中 | 高 |
| 数据一致性 | 弱 | 弱 | 弱 |
| 事务支持 | ✅ | ✅ | 部分 |
| 客户端复杂度 | 低 | 中 | 高 |
4.2 选择建议
主从复制适用场景:
- 读多写少的应用
- 对可用性要求不高
- 简单的数据备份需求
哨兵模式适用场景:
- 需要高可用性
- 数据量不大(单机可承载)
- 自动化运维需求
集群模式适用场景:
- 大数据量存储
- 高并发读写
- 需要水平扩展
- 对性能要求极高
5. 最佳实践
5.1 部署建议
- 网络规划:确保节点间网络稳定
- 资源配置:合理分配 CPU、内存、磁盘
- 监控告警:完善的监控和告警机制
- 备份策略:定期数据备份和恢复测试
- 安全配置:启用认证、网络隔离
5.2 性能优化
# 内存优化
maxmemory 2gb
maxmemory-policy allkeys-lru
# 网络优化
tcp-keepalive 300
timeout 0
# 持久化优化
save 900 1
appendonly yes
appendfsync everysec
5.3 监控指标
- 连接数:connected_clients
- 内存使用:used_memory
- 命令统计:total_commands_processed
- 键空间:keyspace_hits/keyspace_misses
- 复制延迟:master_repl_offset - slave_repl_offset
总结
Redis 提供了从简单到复杂的多种部署架构,每种架构都有其适用场景和特点。选择合适的架构需要综合考虑业务需求、数据规模、可用性要求和运维能力。随着业务的发展,也可以逐步从简单架构向复杂架构演进。