难度:⭐⭐⭐⭐ | 适合人群:想掌握Redis高可用方案的开发者
💥 开场:一次"惨痛"的宕机事故
时间: 周六凌晨2点
地点: 家里(睡梦中)
事件: 告警电话
📱 铃声大作...
运维(急促): "Redis服务器挂了!所有接口都超时了!"
我: "什么???" 😱(瞬间清醒)
运维: "磁盘满了,Redis进程被kill了!"
我: "赶紧重启啊!" 😰
运维: "在重启了,但是..."
5分钟后,Redis重启成功
运维: "重启好了,但用户在说数据丢了..."
我: "怎么会丢?有持久化啊!"
运维: "持久化配置的是RDB,5分钟一次,最后5分钟的数据全丢了..."
我: "......" 😭
第二天,紧急会议:
技术总监: "这次事故暴露了我们的架构问题:单点故障!"
哈吉米: "是的,Redis是单机部署,挂了整个系统就崩了。"
南北绿豆: "我们需要主从架构 + 哨兵机制,实现高可用。"
阿西噶阿西: "主从复制可以备份数据,哨兵可以自动故障转移,我给大家讲讲..."
🎯 第一问:什么是主从复制?
主从架构
主从复制 = Master-Slave(一主多从)
Master(主节点)
/ | \
/ | \
Slave1 Slave2 Slave3(从节点)
角色分工:
Master(主节点):
- 负责写操作
- 负责同步数据到从节点
Slave(从节点):
- 负责读操作
- 从主节点复制数据
- 主节点挂了可以升级为主节点
主从复制的作用
哈吉米: "主从复制有三大作用。"
1. 读写分离
客户端
↓
写请求 → Master(主节点)
读请求 → Slave(从节点,多个)
优势:
- ✅ 分担Master压力
- ✅ 提高并发能力
- ✅ 读性能提升
2. 数据备份
Master(主数据)
↓ 实时同步
Slave(备份数据)
Master宕机:
↓
数据在Slave上
↓
可以恢复
3. 高可用
Master正常:
Master提供服务
Master宕机:
↓
Slave升级为Master
↓
继续提供服务
🔄 第二问:主从复制原理
全量复制
第一次连接时,进行全量复制
sequenceDiagram
participant Slave
participant Master
Slave->>Master: 1. 发送PSYNC命令
Note over Slave: PSYNC ? -1<br/>(首次复制)
Master-->>Slave: 2. 回复FULLRESYNC
Note over Master: FULLRESYNC runid offset
Master->>Master: 3. 执行BGSAVE,生成RDB文件
Note over Master: 后台fork子进程<br/>不影响主进程
Master->>Master: 4. 将生成期间的写命令<br/>缓存到复制缓冲区
Master->>Slave: 5. 发送RDB文件
Note over Slave: 清空旧数据<br/>加载RDB文件
Master->>Slave: 6. 发送复制缓冲区的命令
Note over Slave: 执行命令
Note over Slave,Master: 全量复制完成!
增量复制
全量复制后,后续使用增量复制
sequenceDiagram
participant Client
participant Master
participant Slave
Client->>Master: SET key1 "value1"
Master-->>Client: OK
Master->>Master: 写入复制缓冲区
Master->>Slave: 同步命令:SET key1 "value1"
Client->>Master: DEL key2
Master-->>Client: OK
Master->>Master: 写入复制缓冲区
Master->>Slave: 同步命令:DEL key2
Note over Slave: 实时同步<br/>数据一致
复制过程详解
南北绿豆: "我们详细看看复制过程。"
步骤1:建立连接
# 从节点配置
replicaof 192.168.1.100 6379 # 主节点地址
masterauth password123 # 主节点密码
步骤2:从节点发送PSYNC
从节点向主节点发送PSYNC命令:
PSYNC <runid> <offset>
首次复制:
PSYNC ? -1
断线重连:
PSYNC <之前的runid> <之前的offset>
步骤3:主节点判断
if (从节点是首次复制 || 复制偏移量太旧) {
执行全量复制
} else {
执行增量复制(只发送缺失的命令)
}
步骤4:数据同步
全量复制:
Master:生成RDB → 发送RDB → 发送缓冲区命令
Slave:清空数据 → 加载RDB → 执行命令
增量复制:
Master:发送缺失的命令
Slave:执行命令
🔍 第三问:主从延迟问题
什么是主从延迟?
阿西噶阿西: "主从复制不是瞬间完成的,有延迟!"
场景演示:
sequenceDiagram
participant Client
participant Master
participant Slave
Client->>Master: SET user:1 "张三"
Master-->>Client: OK
Note over Master,Slave: 网络延迟:10ms
Master->>Slave: 同步命令(延迟中...)
Client->>Slave: GET user:1
Slave-->>Client: nil(还没同步到)
Note over Slave: 收到同步命令
Slave->>Slave: SET user:1 "张三"
Client->>Slave: GET user:1
Slave-->>Client: "张三"(现在有了)
问题: 写入后立即读,可能读到旧数据!
延迟原因
1. 网络延迟
- 主从节点网络传输耗时
2. 主节点写入量大
- 复制缓冲区积压
3. 从节点处理慢
- 从节点负载高
4. 大Key复制慢
- 单个key值太大
解决方案
方案1:强制读主节点
@Service
public class UserService {
@Autowired
private RedisTemplate<String, String> masterRedis; // 主节点
@Autowired
private RedisTemplate<String, String> slaveRedis; // 从节点
public void updateUser(User user) {
String key = "user:" + user.getId();
// 写入主节点
masterRedis.opsForValue().set(key, JSON.toJSONString(user));
}
public User getUser(Long userId, boolean forceReadMaster) {
String key = "user:" + userId;
String json;
if (forceReadMaster) {
// 强制读主节点(保证一致性)
json = masterRedis.opsForValue().get(key);
} else {
// 读从节点(性能更好)
json = slaveRedis.opsForValue().get(key);
}
return JSON.parseObject(json, User.class);
}
}
方案2:缓存标记
public void updateUser(User user) {
String key = "user:" + user.getId();
// 写入主节点
masterRedis.opsForValue().set(key, JSON.toJSONString(user));
// 设置一个标记(1秒后过期)
String flagKey = "updated:user:" + user.getId();
masterRedis.opsForValue().set(flagKey, "1", 1, TimeUnit.SECONDS);
}
public User getUser(Long userId) {
String key = "user:" + userId;
String flagKey = "updated:user:" + userId;
// 检查是否刚更新过
if (masterRedis.hasKey(flagKey)) {
// 刚更新,读主节点
return JSON.parseObject(masterRedis.opsForValue().get(key), User.class);
} else {
// 没有更新标记,读从节点
return JSON.parseObject(slaveRedis.opsForValue().get(key), User.class);
}
}
🔭 第四问:哨兵(Sentinel)机制
什么是哨兵?
哨兵 = Sentinel(监控者)
南北绿豆: "哨兵就是Redis的'守护天使',监控Redis健康状态。"
架构图:
Sentinel1 Sentinel2 Sentinel3
↓ ↓ ↓
监控 监控 监控
↓ ↓ ↓
┌──────────────────────────────┐
│ Master │
└──────────────────────────────┘
/ | \
/ | \
Slave1 Slave2 Slave3
哨兵的作用
1. 监控(Monitoring)
- 监控Master和Slave是否正常运行
2. 通知(Notification)
- 当Redis节点出现问题时,通知管理员或其他程序
3. 自动故障转移(Automatic Failover)
- Master宕机时,自动选举一个Slave升级为Master
4. 配置提供者(Configuration Provider)
- 客户端连接哨兵,获取当前Master地址
故障转移流程
完整时序图:
sequenceDiagram
participant Client
participant Sentinel1
participant Sentinel2
participant Sentinel3
participant Master
participant Slave1
participant Slave2
Note over Master: Master正常运行
Sentinel1->>Master: PING
Master-->>Sentinel1: PONG
Note over Master: Master宕机!💥
Sentinel1->>Master: PING
Note over Sentinel1: 超时无响应
Sentinel1->>Sentinel1: 主观下线判断
Note over Sentinel1: Master可能挂了
Sentinel1->>Sentinel2: 询问:Master是否下线?
Sentinel2-->>Sentinel1: 是,我也ping不通
Sentinel1->>Sentinel3: 询问:Master是否下线?
Sentinel3-->>Sentinel1: 是,我也ping不通
Note over Sentinel1,Sentinel3: 客观下线判断<br/>超过半数认为下线
Sentinel1->>Sentinel1: 发起选举
Note over Sentinel1: 我要当Leader进行故障转移
Sentinel2->>Sentinel1: 投票给你
Sentinel3->>Sentinel1: 投票给你
Note over Sentinel1: 成为Leader
Sentinel1->>Slave1: 选举你为新Master
Note over Slave1: Slave1升级为Master
Sentinel1->>Slave2: 重新配置:复制Slave1
Slave2->>Slave1: 开始复制新Master
Sentinel1->>Client: 通知:Master地址变更
Client->>Slave1: 连接新Master
主观下线 vs 客观下线
哈吉米: "哨兵判断节点下线分两步。"
主观下线(SDOWN):
单个哨兵判断:
↓
Sentinel1 PING Master
↓
超时无响应(连续N次)
↓
Sentinel1认为Master主观下线
配置:
# 30秒内PING不通,认为主观下线
sentinel down-after-milliseconds mymaster 30000
客观下线(ODOWN):
多个哨兵判断:
↓
Sentinel1认为Master主观下线
↓
询问其他哨兵
↓
超过半数哨兵认为下线
↓
客观下线(真的挂了)
↓
触发故障转移
配置:
# 至少2个哨兵认为下线才算客观下线
sentinel monitor mymaster 192.168.1.100 6379 2
↑
quorum(法定人数)
⚙️ 第五问:哨兵配置与搭建
配置文件
sentinel.conf:
# 哨兵端口
port 26379
# 工作目录
dir /var/lib/redis/sentinel
# 监控的Master
# sentinel monitor <master-name> <ip> <port> <quorum>
sentinel monitor mymaster 192.168.1.100 6379 2
# 多久无响应算主观下线(毫秒)
sentinel down-after-milliseconds mymaster 30000
# 故障转移超时时间
sentinel failover-timeout mymaster 180000
# 同时进行复制的Slave数量
sentinel parallel-syncs mymaster 1
# Master密码
sentinel auth-pass mymaster password123
# 通知脚本(可选)
sentinel notification-script mymaster /var/redis/notify.sh
# 故障转移脚本(可选)
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
搭建主从 + 哨兵
南北绿豆: "我演示一下搭建过程。"
步骤1:配置Master
# redis-master.conf
bind 0.0.0.0
port 6379
daemonize yes
pidfile /var/run/redis-master.pid
logfile /var/log/redis/redis-master.log
dir /var/lib/redis/master
# 持久化
appendonly yes
appendfsync everysec
步骤2:配置Slave
# redis-slave1.conf
bind 0.0.0.0
port 6380
daemonize yes
pidfile /var/run/redis-slave1.pid
logfile /var/log/redis/redis-slave1.log
dir /var/lib/redis/slave1
# 主从配置
replicaof 192.168.1.100 6379 # 主节点地址
masterauth password123 # 主节点密码
# 从节点只读
replica-read-only yes
# 持久化
appendonly yes
步骤3:配置哨兵(3个)
# sentinel1.conf
port 26379
daemonize yes
pidfile /var/run/redis-sentinel1.pid
logfile /var/log/redis/sentinel1.log
dir /var/lib/redis/sentinel1
sentinel monitor mymaster 192.168.1.100 6379 2
sentinel down-after-milliseconds mymaster 30000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000
sentinel2.conf、sentinel3.conf类似(改端口和路径)
步骤4:启动
# 启动Master
redis-server /etc/redis/redis-master.conf
# 启动Slave
redis-server /etc/redis/redis-slave1.conf
redis-server /etc/redis/redis-slave2.conf
# 启动Sentinel
redis-sentinel /etc/redis/sentinel1.conf
redis-sentinel /etc/redis/sentinel2.conf
redis-sentinel /etc/redis/sentinel3.conf
步骤5:验证
# 连接Master
redis-cli -p 6379
# 查看主从信息
redis> INFO replication
# 输出:
role:master
connected_slaves:2
slave0:ip=192.168.1.101,port=6380,state=online,offset=1234
slave1:ip=192.168.1.102,port=6381,state=online,offset=1234
# 连接Sentinel
redis-cli -p 26379
# 查看Master信息
sentinel> SENTINEL masters
# 查看Slave信息
sentinel> SENTINEL slaves mymaster
# 查看其他Sentinel
sentinel> SENTINEL sentinels mymaster
💻 第六问:Java客户端使用
Jedis连接哨兵
@Configuration
public class RedisConfig {
@Bean
public JedisSentinelPool jedisSentinelPool() {
// 哨兵地址列表
Set<String> sentinels = new HashSet<>();
sentinels.add("192.168.1.100:26379");
sentinels.add("192.168.1.101:26379");
sentinels.add("192.168.1.102:26379");
// 创建连接池
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(100);
poolConfig.setMaxIdle(20);
poolConfig.setMinIdle(10);
// 连接哨兵(master-name、密码、连接池配置)
return new JedisSentinelPool(
"mymaster", // 主节点名称
sentinels, // 哨兵地址
poolConfig, // 连接池配置
2000, // 超时时间
"password123" // 密码
);
}
}
使用:
@Service
public class UserService {
@Autowired
private JedisSentinelPool sentinelPool;
public void setUser(String key, String value) {
try (Jedis jedis = sentinelPool.getResource()) {
jedis.set(key, value);
}
}
public String getUser(String key) {
try (Jedis jedis = sentinelPool.getResource()) {
return jedis.get(key);
}
}
}
优势:
- ✅ 自动发现Master地址
- ✅ Master切换时自动重连
- ✅ 对业务代码透明
Redisson连接哨兵
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
// 哨兵模式配置
config.useSentinelServers()
.setMasterName("mymaster")
.addSentinelAddress(
"redis://192.168.1.100:26379",
"redis://192.168.1.101:26379",
"redis://192.168.1.102:26379"
)
.setPassword("password123")
.setDatabase(0)
.setConnectTimeout(3000)
.setTimeout(3000)
.setRetryAttempts(3)
.setRetryInterval(1500);
return Redisson.create(config);
}
}
🧪 第七问:故障转移实战演示
模拟Master宕机
阿西噶阿西: "我们实际测试一下故障转移。"
初始状态:
# Master
redis-cli -p 6379 INFO replication
role:master
connected_slaves:2
# Slave1
redis-cli -p 6380 INFO replication
role:slave
master_host:192.168.1.100
master_port:6379
# Sentinel
redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
192.168.1.100
6379
模拟宕机:
# 杀掉Master进程
kill -9 <master-pid>
观察哨兵日志:
[26379] 15:30:00 # +sdown master mymaster 192.168.1.100 6379
↓ 主观下线
[26379] 15:30:02 # +odown master mymaster 192.168.1.100 6379 #quorum 2/2
↓ 客观下线
[26379] 15:30:02 # +vote-for-leader abc123def456...
↓ 选举Leader
[26379] 15:30:03 # +elected-leader master mymaster 192.168.1.100 6379
↓ 成为Leader
[26379] 15:30:03 # +failover-state-select-slave master mymaster 192.168.1.100 6379
↓ 选择Slave
[26379] 15:30:03 # +selected-slave slave 192.168.1.101:6380
↓ 选中Slave1
[26379] 15:30:04 # +failover-state-send-slaveof-noone slave 192.168.1.101:6380
↓ 让Slave1成为Master
[26379] 15:30:05 # +failover-state-reconf-slaves master mymaster 192.168.1.100 6379
↓ 重新配置其他Slave
[26379] 15:30:06 # +slave-reconf-sent slave 192.168.1.102:6381
↓ 配置Slave2复制新Master
[26379] 15:30:07 # +failover-end master mymaster 192.168.1.100 6379
↓ 故障转移完成
[26379] 15:30:08 # +switch-master mymaster 192.168.1.100 6379 192.168.1.101 6380
↓ Master地址切换
验证新Master:
# 连接原来的Slave1(现在的Master)
redis-cli -p 6380 INFO replication
role:master # 已经变成Master了
connected_slaves:1 # 有1个从节点
slave0:ip=192.168.1.102,port=6381
客户端自动切换:
// 使用哨兵模式的客户端会自动切换
redissonClient.getBucket("test").set("hello");
// 自动连接到新的Master(192.168.1.101:6380)
整个过程对业务代码透明! ✨
🎯 第八问:哨兵选举规则
选举新Master的规则
哈吉米: "哨兵选举新Master有优先级。"
选举流程:
1. 过滤掉不健康的Slave
- 断线的
- 5秒内没回复PING的
- 与Master断开超过10倍down-after-milliseconds的
2. 选择优先级最高的
- replica-priority配置(默认100)
3. 优先级相同,选择复制偏移量最大的
- 数据最完整的
4. 还相同,选择运行ID最小的
- runid字典序最小
配置优先级:
# slave1.conf
replica-priority 100 # 默认
# slave2.conf
replica-priority 90 # 优先级更高,更容易被选为Master
# slave3.conf
replica-priority 0 # 永远不会被选为Master(只做备份)
💡 最佳实践
1. 哨兵数量
推荐:奇数个(3个或5个)
原因:
- 需要超过半数才能判定客观下线
- 3个哨兵:允许1个挂掉
- 5个哨兵:允许2个挂掉
不推荐:
- 1个哨兵:单点故障
- 2个哨兵:1个挂了达不到半数
- 偶数个:容易脑裂
2. 哨兵部署
✅ 推荐:哨兵部署在不同机器
Sentinel1 → 服务器A
Sentinel2 → 服务器B
Sentinel3 → 服务器C
❌ 不推荐:哨兵和Redis在同一机器
服务器挂了,哨兵和Redis一起挂
3. 主从数量
推荐:
1个Master + 2-3个Slave
原因:
- 2个Slave足够备份
- Slave太多会增加Master同步压力
不推荐:
- 只有1个Slave:备份不够
- 超过5个Slave:同步压力大
4. 读写分离配置
@Configuration
public class RedisConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSentinelServers()
.setMasterName("mymaster")
.addSentinelAddress("redis://192.168.1.100:26379")
.setReadMode(ReadMode.SLAVE) // ← 读从Slave
.setSubscriptionMode(SubscriptionMode.MASTER); // 订阅从Master
return Redisson.create(config);
}
}
ReadMode选项:
MASTER:只读Master
SLAVE:只读Slave
MASTER_SLAVE:Master和Slave都读
💡 知识点总结
主从复制与哨兵核心要点
✅ 主从复制作用
- 读写分离(提高性能)
- 数据备份(防止丢失)
- 高可用(故障转移)
✅ 复制原理
- 全量复制(首次):BGSAVE生成RDB
- 增量复制(后续):同步写命令
- 复制缓冲区:暂存命令
✅ 主从延迟
- 网络延迟
- 主节点写入量大
- 解决:强制读主、缓存标记
✅ 哨兵机制
- 监控Redis健康
- 自动故障转移
- 配置提供者
✅ 故障转移流程
- 主观下线(单个哨兵判断)
- 客观下线(多数哨兵判断)
- 选举Leader哨兵
- 选举新Master
- 重新配置Slave
- 通知客户端
✅ 最佳实践
- 哨兵:奇数个(3或5)
- 哨兵:不同机器部署
- Slave:2-3个
- 读写分离:读从Slave
记忆口诀
主从复制分角色,
Master写Slave读。
全量复制传RDB,
增量复制传命令。
哨兵监控来守护,
故障转移自动搞。
主观客观两判断,
超过半数才算数。
选举新主有规则,
优先级高复制全。
生产至少三哨兵,
奇数部署在不同机。
🤔 常见面试题
Q1: 主从复制的流程?
A:
全量复制(首次):
1. Slave发送PSYNC ? -1
2. Master回复FULLRESYNC
3. Master执行BGSAVE生成RDB
4. Master发送RDB文件给Slave
5. Slave加载RDB文件
6. Master发送复制缓冲区的命令
7. Slave执行命令
8. 全量复制完成
增量复制(后续):
1. Master有写操作
2. 写入复制缓冲区
3. 异步发送给Slave
4. Slave执行命令
Q2: 哨兵如何判断Master下线?
A:
两步判断:
1. 主观下线(SDOWN)
- 单个Sentinel PING Master
- 超过down-after-milliseconds无响应
- 认为主观下线
2. 客观下线(ODOWN)
- Sentinel询问其他Sentinel
- 超过quorum个Sentinel认为下线
- 确认客观下线
- 触发故障转移
配置:
sentinel monitor mymaster 192.168.1.100 6379 2
↑
quorum=2
Q3: 哨兵如何选举新Master?
A:
选举规则(优先级从高到低):
1. 过滤不健康的Slave
- 断线的
- 长时间未响应的
2. 选择replica-priority最高的
3. 优先级相同,选择复制偏移量最大的
- 数据最完整
4. 还相同,选择runid最小的
最终只有一个Slave被选为Master
Q4: 主从复制有什么缺点?
A:
缺点:
1. 主从延迟
- 写入Master后,Slave可能还没同步
- 立即读Slave可能读到旧数据
2. Master写压力
- 所有写操作都在Master
- Master是瓶颈
3. 容量限制
- 所有节点存储全量数据
- 数据量大时,单机内存不够
解决:
- 延迟:读主节点、缓存标记
- 写压力:Redis Cluster分片
- 容量:Redis Cluster分片
💬 写在最后
从单机到主从,再到哨兵,我们深入学习了Redis的高可用架构:
- 🔄 理解了主从复制的原理和流程
- 🔭 掌握了哨兵的监控和故障转移
- ⚙️ 学会了哨兵的配置和搭建
- 💻 完成了Java客户端的使用
这篇文章,希望能让你的Redis架构更加稳定可靠!
如果这篇文章对你有帮助,请:
- 👍 点赞支持
- ⭐ 收藏备用
- 🔄 转发分享
- 💬 评论交流
下一篇我们聊Redis Cluster集群! 👋