🔄 Redis 主从复制

构建高可用的 Redis 架构

主从复制概述

Redis 主从复制是一种数据冗余和读写分离的解决方案。通过将数据从主服务器(Master)复制到一个或多个从服务器(Slave),实现数据备份、读写分离和负载均衡。

主从复制架构

🔴 Master

主服务器

读写操作

🟢 Slave 1

从服务器

只读操作

🟢 Slave 2

从服务器

只读操作

特点:一主多从、异步复制、读写分离

🔧 复制配置

配置 Redis 主从复制的方法:

主服务器配置:

# redis-master.conf
# 绑定IP地址
bind 0.0.0.0
port 6379

# 设置密码(可选)
requirepass masterpassword

# 持久化配置
save 900 1
save 300 10
save 60 10000
appendonly yes

# 复制相关配置
# 设置复制积压缓冲区大小
repl-backlog-size 1mb
# 设置复制积压缓冲区TTL
repl-backlog-ttl 3600

# 慢查询日志
slowlog-log-slower-than 10000
slowlog-max-len 128

从服务器配置:

# redis-slave.conf
# 绑定IP地址
bind 0.0.0.0
port 6380

# 指定主服务器
slaveof 192.168.1.100 6379
# 或使用新的配置项
replicaof 192.168.1.100 6379

# 主服务器密码
masterauth masterpassword

# 从服务器只读
slave-read-only yes
# 或使用新的配置项
replica-read-only yes

# 从服务器密码(可选)
requirepass slavepassword

# 复制相关配置
# 复制超时时间
repl-timeout 60
# 禁用TCP_NODELAY
repl-disable-tcp-nodelay no

# 当主从连接断开时的行为
slave-serve-stale-data yes
replica-serve-stale-data yes

动态配置复制:

# 在从服务器上执行
# 设置主服务器
SLAVEOF 192.168.1.100 6379
# 或使用新命令
REPLICAOF 192.168.1.100 6379

# 取消复制(变为独立服务器)
SLAVEOF NO ONE
REPLICAOF NO ONE

# 查看复制信息
INFO replication

# 查看主从延迟
INFO replication | grep lag

🔄 复制原理

Redis 主从复制的工作流程:

1. 连接

从服务器连接主服务器

2. 握手

发送PING命令验证连接

3. 认证

发送AUTH命令认证

4. 同步

全量同步或增量同步

5. 复制

持续接收命令流

全量同步(Full Resynchronization):

# 全量同步流程
1. 从服务器发送 PSYNC ? -1 命令
2. 主服务器执行 BGSAVE 生成 RDB 文件
3. 主服务器将 RDB 文件发送给从服务器
4. 从服务器清空数据库并载入 RDB 文件
5. 主服务器将复制积压缓冲区的命令发送给从服务器

# 触发全量同步的情况:
- 首次连接
- 复制ID不匹配
- 偏移量不在积压缓冲区范围内

增量同步(Partial Resynchronization):

# 增量同步流程
1. 从服务器发送 PSYNC   命令
2. 主服务器检查复制ID和偏移量
3. 如果偏移量在积压缓冲区范围内,发送 +CONTINUE
4. 主服务器发送积压缓冲区中的命令

# 增量同步的优势:
- 减少网络传输
- 降低主服务器负载
- 缩短同步时间

📊 复制监控

监控主从复制状态的重要指标:

监控指标 命令 说明
复制状态 INFO replication 查看主从复制详细信息
复制延迟 INFO replication | grep lag 查看主从延迟时间
连接状态 INFO replication | grep state 查看连接状态
复制偏移量 INFO replication | grep offset 查看复制进度
从服务器数量 INFO replication | grep slaves 查看从服务器数量

复制状态监控示例:

# 主服务器信息
redis-cli INFO replication
# role:master
# connected_slaves:2
# slave0:ip=192.168.1.101,port=6379,state=online,offset=1234567,lag=0
# slave1:ip=192.168.1.102,port=6379,state=online,offset=1234567,lag=1
# master_repl_offset:1234567
# repl_backlog_active:1
# repl_backlog_size:1048576

# 从服务器信息
redis-cli -p 6380 INFO replication
# role:slave
# master_host:192.168.1.100
# master_port:6379
# master_link_status:up
# master_last_io_seconds_ago:0
# master_sync_in_progress:0
# slave_repl_offset:1234567
# slave_priority:100
# slave_read_only:1

复制监控脚本:

#!/bin/bash
# Redis 主从复制监控脚本

MASTER_HOST="192.168.1.100"
MASTER_PORT="6379"
SLAVE_HOSTS=("192.168.1.101" "192.168.1.102")
SLAVE_PORT="6379"
LOG_FILE="/var/log/redis-replication.log"

# 检查主服务器状态
master_info=$(redis-cli -h $MASTER_HOST -p $MASTER_PORT INFO replication)
connected_slaves=$(echo "$master_info" | grep "connected_slaves" | cut -d: -f2)
echo "$(date): Master connected slaves: $connected_slaves" >> $LOG_FILE

# 检查从服务器状态
for slave_host in "${SLAVE_HOSTS[@]}"; do
    slave_info=$(redis-cli -h $slave_host -p $SLAVE_PORT INFO replication)
    link_status=$(echo "$slave_info" | grep "master_link_status" | cut -d: -f2)
    lag=$(echo "$slave_info" | grep "master_last_io_seconds_ago" | cut -d: -f2)
    
    echo "$(date): Slave $slave_host - Link: $link_status, Lag: ${lag}s" >> $LOG_FILE
    
    # 告警检查
    if [ "$link_status" != "up" ]; then
        echo "ALERT: Slave $slave_host is disconnected!" >> $LOG_FILE
    fi
    
    if [ "$lag" -gt 10 ]; then
        echo "ALERT: Slave $slave_host lag is too high: ${lag}s" >> $LOG_FILE
    fi
done

⚡ 读写分离

利用主从复制实现读写分离,提升系统性能:

应用层读写分离:

# Python 示例
import redis

class RedisCluster:
    def __init__(self):
        # 主服务器(写操作)
        self.master = redis.Redis(
            host='192.168.1.100',
            port=6379,
            password='masterpassword',
            decode_responses=True
        )
        
        # 从服务器(读操作)
        self.slaves = [
            redis.Redis(
                host='192.168.1.101',
                port=6379,
                password='slavepassword',
                decode_responses=True
            ),
            redis.Redis(
                host='192.168.1.102',
                port=6379,
                password='slavepassword',
                decode_responses=True
            )
        ]
        self.slave_index = 0
    
    def write(self, key, value):
        """写操作,使用主服务器"""
        return self.master.set(key, value)
    
    def read(self, key):
        """读操作,使用从服务器(负载均衡)"""
        slave = self.slaves[self.slave_index]
        self.slave_index = (self.slave_index + 1) % len(self.slaves)
        return slave.get(key)
    
    def read_with_fallback(self, key):
        """读操作,带故障转移"""
        for slave in self.slaves:
            try:
                return slave.get(key)
            except redis.ConnectionError:
                continue
        # 所有从服务器都不可用,使用主服务器
        return self.master.get(key)

# 使用示例
cluster = RedisCluster()
cluster.write('user:1001', 'Alice')  # 写入主服务器
value = cluster.read('user:1001')    # 从从服务器读取

Java 示例(使用 Jedis):

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class RedisCluster {
    private JedisPool masterPool;
    private JedisPool[] slavePools;
    private int slaveIndex = 0;
    
    public RedisCluster() {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(100);
        config.setMaxIdle(20);
        
        // 主服务器连接池
        masterPool = new JedisPool(config, "192.168.1.100", 6379, 2000, "masterpassword");
        
        // 从服务器连接池
        slavePools = new JedisPool[]{
            new JedisPool(config, "192.168.1.101", 6379, 2000, "slavepassword"),
            new JedisPool(config, "192.168.1.102", 6379, 2000, "slavepassword")
        };
    }
    
    public String write(String key, String value) {
        try (Jedis jedis = masterPool.getResource()) {
            return jedis.set(key, value);
        }
    }
    
    public String read(String key) {
        JedisPool slavePool = slavePools[slaveIndex];
        slaveIndex = (slaveIndex + 1) % slavePools.length;
        
        try (Jedis jedis = slavePool.getResource()) {
            return jedis.get(key);
        } catch (Exception e) {
            // 从服务器异常,使用主服务器
            try (Jedis jedis = masterPool.getResource()) {
                return jedis.get(key);
            }
        }
    }
}

主从复制优缺点

方面 优点 缺点
可用性 数据备份,故障转移 主服务器单点故障
性能 读写分离,负载分担 复制延迟
扩展性 水平扩展读能力 写能力无法扩展
一致性 最终一致性 可能读到旧数据
复杂性 配置简单 需要应用层支持

🚨 故障处理

主从复制环境中的故障处理策略:

主服务器故障:

⚠️ 主服务器故障处理:

  1. 检测故障:监控主服务器状态
  2. 选择新主:从从服务器中选择一个作为新主
  3. 提升为主:执行 SLAVEOF NO ONE
  4. 重新配置:其他从服务器指向新主
  5. 应用切换:修改应用配置指向新主

故障转移脚本:

#!/bin/bash
# Redis 主服务器故障转移脚本

MASTER_HOST="192.168.1.100"
MASTER_PORT="6379"
NEW_MASTER_HOST="192.168.1.101"
NEW_MASTER_PORT="6379"
SLAVE_HOSTS=("192.168.1.102")

# 检查主服务器状态
if ! redis-cli -h $MASTER_HOST -p $MASTER_PORT ping > /dev/null 2>&1; then
    echo "Master server is down, starting failover..."
    
    # 提升从服务器为主服务器
    redis-cli -h $NEW_MASTER_HOST -p $NEW_MASTER_PORT SLAVEOF NO ONE
    echo "Promoted $NEW_MASTER_HOST to master"
    
    # 重新配置其他从服务器
    for slave_host in "${SLAVE_HOSTS[@]}"; do
        redis-cli -h $slave_host -p $MASTER_PORT SLAVEOF $NEW_MASTER_HOST $NEW_MASTER_PORT
        echo "Reconfigured $slave_host to follow new master"
    done
    
    echo "Failover completed"
else
    echo "Master server is healthy"
fi

从服务器故障:

💡 从服务器故障处理:

  • 自动检测:应用层检测从服务器连接状态
  • 流量切换:将读请求转发到其他从服务器
  • 故障恢复:修复从服务器后自动重新加入
  • 数据同步:从服务器重启后自动同步数据

网络分区处理:

# 配置最小从服务器数量
# 当连接的从服务器少于指定数量时,主服务器停止写入
min-slaves-to-write 1
min-slaves-max-lag 10

# 这样可以避免网络分区时的数据不一致问题

🔧 性能优化

主从复制的性能优化建议:

网络优化:

  • 禁用 TCP_NODELAY:设置 repl-disable-tcp-nodelay no
  • 调整缓冲区:增大 repl-backlog-size
  • 网络带宽:确保主从之间有足够的网络带宽
  • 就近部署:主从服务器部署在同一数据中心

内存优化:

  • 复制积压缓冲区:根据网络延迟调整大小
  • 输出缓冲区:调整 client-output-buffer-limit
  • 内存策略:合理设置 maxmemory-policy

磁盘优化:

  • RDB 优化:调整 RDB 生成频率
  • AOF 优化:选择合适的 AOF 同步策略
  • SSD 存储:使用 SSD 提升 I/O 性能

性能优化配置:

# 主服务器优化配置
# 复制积压缓冲区(根据网络延迟调整)
repl-backlog-size 10mb
repl-backlog-ttl 3600

# 输出缓冲区限制
client-output-buffer-limit slave 256mb 64mb 60

# 禁用慢查询对复制的影响
repl-disable-tcp-nodelay no

# 从服务器优化配置
# 复制超时时间
repl-timeout 60

# 当主从连接断开时继续提供服务
slave-serve-stale-data yes

# 从服务器优先级(用于故障转移)
slave-priority 100

💡 最佳实践:

  • 监控复制延迟:设置告警阈值,及时发现问题
  • 定期备份:除了复制,还要定期备份 RDB 文件
  • 读写分离:在应用层实现读写分离逻辑
  • 故障转移:准备自动化的故障转移方案
  • 容量规划:根据业务增长预估从服务器数量
  • 安全配置:设置密码,限制网络访问