Redis

Redis 核心知识笔记 - 第三部分:高可用架构

Redis 核心知识笔记 - 第三部分:高可用架构

一、高可用架构概述

模式特点适用场景
主从复制数据备份、读写分离读多写少,数据量适中
哨兵模式自动故障转移、高可用中小规模高可用需求
Cluster 集群分片存储、水平扩展大数据量、高并发场景

二、主从复制

2.1 主从复制原理

2.2 全量复制 vs 部分复制

2.3 主从复制配置

主节点配置(Master):

# redis.conf
bind 0.0.0.0
port 6379
daemonize yes
pidfile /var/run/redis_6379.pid
logfile "/var/log/redis/redis_6379.log"
dbfilename dump_6379.rdb
dir /var/lib/redis

# 设置密码(可选)
requirepass master_password
masterauth master_password  # 如果主从都设密码

# 复制积压缓冲区大小(支持部分复制)
repl-backlog-size 256mb
repl-backlog-ttl 3600

从节点配置(Slave):

# redis.conf
bind 0.0.0.0
port 6380
daemonize yes
pidfile /var/run/redis_6380.pid
logfile "/var/log/redis/redis_6380.log"
dbfilename dump_6380.rdb
dir /var/lib/redis

# 配置主节点
replicaof 192.168.1.100 6379

# 主节点密码
masterauth master_password

# 从节点只读(默认)
replica-read-only yes

# 从节点是否提供过期数据
replica-serve-stale-data yes

动态设置主从关系:

# 设置为某个主节点的从节点
REPLICAOF 192.168.1.100 6379

# 取消主从关系,变为独立主节点
REPLICAOF NO ONE

# 查看复制状态
INFO replication

2.4 读写分离架构

2.5 Java 读写分离代码

@Configuration
public class RedisReadWriteConfig {
    
    @Bean
    public LettuceConnectionFactory masterConnectionFactory() {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
        config.setHostName("192.168.1.100");
        config.setPort(6379);
        config.setPassword("master_password");
        return new LettuceConnectionFactory(config);
    }
    
    @Bean
    public LettuceConnectionFactory slaveConnectionFactory() {
        // 配置从节点(可配置多个进行负载均衡)
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
        config.setHostName("192.168.1.101");
        config.setPort(6380);
        config.setPassword("master_password");
        
        LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
            .readFrom(ReadFrom.REPLICA_PREFERRED)  // 优先从从节点读取
            .build();
        
        return new LettuceConnectionFactory(config, clientConfig);
    }
    
    @Bean("masterRedisTemplate")
    public StringRedisTemplate masterRedisTemplate(
            @Qualifier("masterConnectionFactory") LettuceConnectionFactory factory) {
        return new StringRedisTemplate(factory);
    }
    
    @Bean("slaveRedisTemplate") 
    public StringRedisTemplate slaveRedisTemplate(
            @Qualifier("slaveConnectionFactory") LettuceConnectionFactory factory) {
        return new StringRedisTemplate(factory);
    }
}

@Service
public class ReadWriteSplitService {
    
    @Autowired
    @Qualifier("masterRedisTemplate")
    private StringRedisTemplate masterTemplate;
    
    @Autowired
    @Qualifier("slaveRedisTemplate")
    private StringRedisTemplate slaveTemplate;
    
    // 写操作走主节点
    public void write(String key, String value) {
        masterTemplate.opsForValue().set(key, value);
    }
    
    // 读操作走从节点
    public String read(String key) {
        return slaveTemplate.opsForValue().get(key);
    }
}

2.6 主从复制优缺点

优点:

  • ✅ 数据热备份,提高数据安全性
  • ✅ 读写分离,提升读性能
  • ✅ 配置简单,易于实现

缺点:

  • ❌ 主节点故障需手动切换
  • ❌ 写操作无法扩展(单主节点)
  • ❌ 全量复制时性能开销大

三、哨兵模式(Sentinel)

3.1 哨兵架构

3.2 哨兵功能

功能描述
监控持续检查主从节点是否正常运行
通知节点出现问题时通知管理员或其他程序
自动故障转移主节点故障时自动选举新的主节点
配置提供者客户端通过哨兵获取当前主节点地址

3.3 故障转移流程

3.4 从节点选举规则

3.5 哨兵配置

sentinel.conf 配置:

# 哨兵端口
port 26379

# 守护进程运行
daemonize yes

# 日志文件
logfile "/var/log/redis/sentinel.log"

# 监控的主节点
# sentinel monitor <master-name> <ip> <port> <quorum>
sentinel monitor mymaster 192.168.1.100 6379 2

# 主节点密码
sentinel auth-pass mymaster master_password

# 主观下线判断时间(毫秒)
sentinel down-after-milliseconds mymaster 30000

# 故障转移超时时间(毫秒)
sentinel failover-timeout mymaster 180000

# 同时进行复制的从节点数量
sentinel parallel-syncs mymaster 1

# 通知脚本(可选)
sentinel notification-script mymaster /opt/redis/notify.sh

# 故障转移后执行的脚本(可选)
sentinel client-reconfig-script mymaster /opt/redis/reconfig.sh

启动哨兵:

# 方式1
redis-sentinel /etc/redis/sentinel.conf

# 方式2
redis-server /etc/redis/sentinel.conf --sentinel

3.6 Java 连接哨兵

@Configuration
public class RedisSentinelConfig {
    
    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
            .master("mymaster")
            .sentinel("192.168.1.100", 26379)
            .sentinel("192.168.1.101", 26379)
            .sentinel("192.168.1.102", 26379);
        
        sentinelConfig.setPassword("master_password");
        
        LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
            .readFrom(ReadFrom.REPLICA_PREFERRED)
            .build();
        
        return new LettuceConnectionFactory(sentinelConfig, clientConfig);
    }
    
    @Bean
    public StringRedisTemplate stringRedisTemplate(LettuceConnectionFactory factory) {
        return new StringRedisTemplate(factory);
    }
}

Spring Boot 配置方式:

spring:
  redis:
    sentinel:
      master: mymaster
      nodes:
        - 192.168.1.100:26379
        - 192.168.1.101:26379
        - 192.168.1.102:26379
    password: master_password
    lettuce:
      pool:
        max-active: 100
        max-idle: 50
        min-idle: 10

3.7 哨兵优缺点

优点:

  • ✅ 自动故障转移,高可用
  • ✅ 配置相对简单
  • ✅ 客户端自动发现主节点

缺点:

  • ❌ 写操作仍然只能单点
  • ❌ 存储容量受单机限制
  • ❌ 故障转移期间短暂不可用

四、Cluster 集群模式

4.1 Cluster 架构

4.2 数据分片原理

槽位(Slot)分配:

  • Redis Cluster 将数据划分为 16384 个槽位
  • 每个主节点负责一部分槽位
  • 数据根据 Key 的 CRC16 值决定存储在哪个槽位

槽位计算公式:

SLOT = CRC16(key) % 16384

Hash Tag 机制:

# 使用 {} 标记 Hash Tag,相同 Tag 的 Key 会分配到同一槽位
SET {user:1001}:name "张三"
SET {user:1001}:age "25"
SET {user:1001}:email "[email protected]"

# 这三个 Key 都会根据 "user:1001" 计算槽位
# 从而保证存储在同一节点,支持 MGET 等多 Key 操作

4.3 Cluster 配置

节点配置 (node-6379.conf):

port 6379
daemonize yes
pidfile /var/run/redis_6379.pid
logfile "/var/log/redis/redis_6379.log"
dbfilename dump_6379.rdb
dir /var/lib/redis

# 开启集群模式
cluster-enabled yes

# 集群配置文件(自动生成)
cluster-config-file nodes-6379.conf

# 节点超时时间
cluster-node-timeout 15000

# 主节点无从节点时是否继续服务
cluster-require-full-coverage no

# 密码
requirepass cluster_password
masterauth cluster_password

创建集群:

# 启动所有节点
redis-server /etc/redis/node-6379.conf
redis-server /etc/redis/node-6380.conf
redis-server /etc/redis/node-6381.conf
redis-server /etc/redis/node-6382.conf
redis-server /etc/redis/node-6383.conf
redis-server /etc/redis/node-6384.conf

# 创建集群(3主3从)
redis-cli --cluster create \
    192.168.1.100:6379 \
    192.168.1.101:6379 \
    192.168.1.102:6379 \
    192.168.1.100:6380 \
    192.168.1.101:6380 \
    192.168.1.102:6380 \
    --cluster-replicas 1 \
    -a cluster_password

4.4 集群管理命令

# 查看集群信息
redis-cli -c -a password cluster info

# 查看节点列表
redis-cli -c -a password cluster nodes

# 查看槽位分配
redis-cli -c -a password cluster slots

# 手动故障转移(在从节点执行)
redis-cli -c -a password cluster failover

# 添加节点
redis-cli --cluster add-node new_host:port existing_host:port

# 删除节点
redis-cli --cluster del-node host:port node_id

# 重新分片
redis-cli --cluster reshard host:port

4.5 故障转移机制

4.6 动态扩容

扩容步骤:

# 1. 启动新节点
redis-server /etc/redis/node-new.conf

# 2. 加入集群
redis-cli --cluster add-node 192.168.1.103:6379 192.168.1.100:6379

# 3. 重新分片(分配槽位)
redis-cli --cluster reshard 192.168.1.100:6379

# 4. 添加从节点
redis-cli --cluster add-node 192.168.1.103:6380 192.168.1.103:6379 \
    --cluster-slave --cluster-master-id <master-node-id>

4.7 Java 连接 Cluster

@Configuration
public class RedisClusterConfig {
    
    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration();
        clusterConfig.addClusterNode(new RedisNode("192.168.1.100", 6379));
        clusterConfig.addClusterNode(new RedisNode("192.168.1.101", 6379));
        clusterConfig.addClusterNode(new RedisNode("192.168.1.102", 6379));
        clusterConfig.setPassword("cluster_password");
        clusterConfig.setMaxRedirects(3);
        
        LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
            .readFrom(ReadFrom.REPLICA_PREFERRED)
            .build();
        
        return new LettuceConnectionFactory(clusterConfig, clientConfig);
    }
}

Spring Boot 配置:

spring:
  redis:
    cluster:
      nodes:
        - 192.168.1.100:6379
        - 192.168.1.101:6379
        - 192.168.1.102:6379
      max-redirects: 3
    password: cluster_password
    lettuce:
      pool:
        max-active: 100

4.8 Cluster 优缺点

优点:

  • ✅ 数据自动分片,线性扩展
  • ✅ 高可用,自动故障转移
  • ✅ 去中心化,无 SPOF
  • ✅ 支持在线扩容缩容

缺点:

  • ❌ 不支持跨槽位的多 Key 操作(需用 Hash Tag)
  • ❌ 架构复杂,运维成本高
  • ❌ 集群最少 3 主节点
  • ❌ 不支持 SELECT 切换数据库

五、脑裂问题与解决方案

5.1 什么是脑裂

5.2 脑裂解决方案

# redis.conf 配置

# 最少从节点数量(写操作需要至少N个从节点在线)
min-replicas-to-write 1

# 从节点最大延迟时间(秒)
min-replicas-max-lag 10

# 作用:
# 当从节点数量少于 min-replicas-to-write
# 或者从节点延迟超过 min-replicas-max-lag 秒时
# 主节点拒绝写入操作,避免脑裂时数据丢失

六、数据一致性问题

6.1 主从延迟

问题:从节点数据可能落后于主节点

解决方案:

@Service
public class ConsistencyService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    // 方案1:强制读主
    public String readFromMaster(String key) {
        // 配置连接池只连主节点
        return redisTemplate.opsForValue().get(key);
    }
    
    // 方案2:等待复制完成
    public void writeWithWait(String key, String value, int numReplicas, long timeout) {
        redisTemplate.opsForValue().set(key, value);
        
        // WAIT 命令:等待至少 numReplicas 个从节点确认
        redisTemplate.execute((RedisCallback<Long>) connection -> 
            connection.waitReplicas(numReplicas, timeout)
        );
    }
    
    // 方案3:读己之写(Read Your Writes)
    public String readYourWrites(String key, String sessionId) {
        // 先尝试从本地缓存读取刚写入的值
        String localValue = localCache.get(sessionId + ":" + key);
        if (localValue != null) {
            return localValue;
        }
        // 降级到 Redis 读取
        return redisTemplate.opsForValue().get(key);
    }
}

6.2 数据一致性保证级别

级别实现方式性能一致性
最终一致性异步复制(默认)最高最低
弱一致性读主写主较高较高
强一致性WAIT 命令同步较低最高

七、架构选型对比

维度主从复制哨兵模式Cluster
数据量< 10GB< 10GB> 10GB
高可用手动切换自动切换自动切换
写扩展不支持不支持支持
读扩展支持支持支持
复杂度
适用场景开发测试中小型生产大型生产