Redis进阶

Redis进阶

Redis - deyang - 博客园

Redis7 —— 超详细操作演示!_redis linux-CSDN博客

主从配置

配置

解压后复制三份:Master\Slave1\Slave2

里面配置修改redis.windows.conf

# Master
port 6379

# Slave1
port 6380
slaveof 127.0.0.1 6379
# Slave2
port 6381
slaveof 127.0.0.1 6379

image-20251119112616551

# 服务器开启:
cd Redis-x64-3.0.504 - Master
redis-server redis.windows.conf
# 客户端开启:
cd Redis-x64-3.0.504 - Master
redis-cli -p 6379

只配置主从结点:

  • 数据备份:从节点有主节点的完整数据副本。

  • 读写分离:读操作可以分发到从节点,减轻主节点压力。

    • 一般情况下,主节点可以读写,从节点只能读
  • 主节点宕机的情况下:

    • 如果应用程序只配置了连接主节点,所有读写都不可用,配置了连接从节点,则可以在从节点读,但是都不能写了。

    • 没有哨兵只能一直派人值守,发现主节点宕机,手动选择新的主结点,否则一直无法写。

    • 这就是为什么需要哨兵:它自动完成所有这些决策和操作,实现秒级故障转移!

Jedis读写分离工具类 RedisReadWriteSplit.java

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class RedisReadWriteSplit {
    private JedisPool masterPool;      // 主节点连接池 - 写操作
    private List<JedisPool> slavePools; // 从节点连接池列表 - 读操作
    private Random random = new Random();
    
    public RedisReadWriteSplit() {
        // 连接池配置
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(50);        // 最大连接数
        poolConfig.setMaxIdle(10);         // 最大空闲连接
        poolConfig.setMinIdle(5);          // 最小空闲连接
        poolConfig.setMaxWaitMillis(3000); // 最大等待时间
        
        // 主节点连接池(写操作)
        masterPool = new JedisPool(poolConfig, "127.0.0.1", 6379, 3000, "123456");
        
        // 从节点连接池(读操作)
        slavePools = new ArrayList<>();
        slavePools.add(new JedisPool(poolConfig, "127.0.0.1", 6380, 3000, "123456"));
        slavePools.add(new JedisPool(poolConfig, "127.0.0.1", 6381, 3000, "123456"));
    }
    
    /**
     * 写操作 - 使用主节点
     */
    public String set(String key, String value) {
        try (Jedis jedis = masterPool.getResource()) {
            return jedis.set(key, value);
        }
    }
    
    public Long del(String key) {
        try (Jedis jedis = masterPool.getResource()) {
            return jedis.del(key);
        }
    }
    
    public Long incr(String key) {
        try (Jedis jedis = masterPool.getResource()) {
            return jedis.incr(key);
        }
    }
    
    /**
     * 读操作 - 随机选择从节点
     */
    public String get(String key) {
        JedisPool slavePool = getRandomSlavePool();
        try (Jedis jedis = slavePool.getResource()) {
            return jedis.get(key);
        }
    }
    
    public Boolean exists(String key) {
        JedisPool slavePool = getRandomSlavePool();
        try (Jedis jedis = slavePool.getResource()) {
            return jedis.exists(key);
        }
    }
    
    /**
     * 随机获取一个从节点连接池
     */
    private JedisPool getRandomSlavePool() {
        int index = random.nextInt(slavePools.size());
        return slavePools.get(index);
    }
    
    /**
     * 关闭所有连接池
     */
    public void close() {
        if (masterPool != null) {
            masterPool.close();
        }
        for (JedisPool pool : slavePools) {
            if (pool != null) {
                pool.close();
            }
        }
    }
}
// 测试类
public class RedisDemo {
    public static void main(String[] args) {
        RedisReadWriteSplit redis = new RedisReadWriteSplit();
        
        try {
            // 1. 写操作演示 - 使用主节点
            System.out.println("=== 写操作演示 ===");
            redis.set("user:1001", "张三");
            redis.set("user:1002", "李四");
            redis.incr("visit_count");
            
            System.out.println("数据写入成功");
            
            // 2. 读操作演示 - 使用从节点
            System.out.println("\n=== 读操作演示 ===");
            for (int i = 0; i < 5; i++) {
                String user1 = redis.get("user:1001");
                String user2 = redis.get("user:1002");
                String count = redis.get("visit_count");
                
                System.out.println(String.format("读取 %d: user1001=%s, user1002=%s, count=%s", 
                    i + 1, user1, user2, count));
            }
            
            // 3. 更多操作示例
            System.out.println("\n=== 更多操作示例 ===");
            
            // Hash操作
            redis.set("user:1001:profile", "{\"name\":\"张三\",\"age\":25}");
            
            // List操作
            try (Jedis jedis = redis.getMasterResource()) {
                jedis.lpush("news:latest", "新闻1", "新闻2", "新闻3");
            }
            
            // 从从节点读取List
            String latestNews = redis.getSlaveResource().lpop("news:latest");
            System.out.println("最新新闻: " + latestNews);
            
        } finally {
            redis.close();
        }
    }
}

与Redis Cluster对比

import redis.clients.jedis.*;
import redis.clients.jedis.exceptions.JedisConnectionException;
import java.util.HashSet;
import java.util.Set;

public class RedisClusterClient {
    private JedisCluster jedisCluster;
    
    // 初始化连接
    public void init() {
        try {
            // 1. 连接池配置
            JedisPoolConfig poolConfig = new JedisPoolConfig();
            poolConfig.setMaxTotal(200);          // 最大连接数
            poolConfig.setMaxIdle(50);            // 最大空闲连接
            poolConfig.setMinIdle(10);            // 最小空闲连接
            poolConfig.setMaxWaitMillis(3000);    // 获取连接最大等待时间
            poolConfig.setTestOnBorrow(true);     // 获取连接时测试
            poolConfig.setTestOnReturn(true);     // 归还连接时测试
            poolConfig.setTestWhileIdle(true);    // 空闲时测试
            
            // 2. Redis Cluster 节点
            Set<HostAndPort> nodes = new HashSet<>();
            nodes.add(new HostAndPort("192.168.43.212", 7001)); // 主节点
            nodes.add(new HostAndPort("192.168.43.212", 7002)); // 从节点
            nodes.add(new HostAndPort("192.168.43.213", 7001)); // 主节点
            nodes.add(new HostAndPort("192.168.43.213", 7002)); // 从节点
            nodes.add(new HostAndPort("192.168.43.214", 7001)); // 主节点
            nodes.add(new HostAndPort("192.168.43.214", 7002)); // 从节点
            
            // 3. 创建 JedisCluster
            // 参数:节点集合, 连接超时, 读取超时, 最大重试次数, 密码, 连接池配置
            jedisCluster = new JedisCluster(
                nodes,
                2000,          // connectionTimeout
                2000,          // soTimeout
                3,             // maxAttempts 重试次数
                "redis123",    // password (没有密码传null)
                poolConfig
            );
            
            // 测试连接
            testConnection();
            
        } catch (Exception e) {
            throw new RuntimeException("Redis Cluster 连接失败", e);
        }
    }
    
    private void testConnection() {
        try {
            String pong = jedisCluster.ping();
            System.out.println("Redis Cluster 连接成功: " + pong);
        } catch (Exception e) {
            throw new RuntimeException("Redis Cluster 连接测试失败", e);
        }
    }
    
    // 基本操作示例
    public void basicOperations() {
        try {
            // 设置值
            jedisCluster.set("user:1001:name", "张三");
            jedisCluster.set("user:1001:age", "25");
            
            // 获取值
            String name = jedisCluster.get("user:1001:name");
            System.out.println("用户名: " + name);
            
            // 哈希操作
            jedisCluster.hset("product:2001", "name", "iPhone");
            jedisCluster.hset("product:2001", "price", "6999");
            
            // 列表操作
            jedisCluster.lpush("queue:orders", "order1", "order2", "order3");
            
            // 设置过期时间
            jedisCluster.expire("user:1001:name", 3600);
            
            // 批量操作(注意:批量操作需要在同一个slot)
            jedisCluster.mset(
                "{user:1001}:email", "zhangsan@example.com",
                "{user:1001}:phone", "13800138000"
            );
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    // 关闭连接
    public void close() {
        if (jedisCluster != null) {
            try {
                jedisCluster.close();
                System.out.println("Redis Cluster 连接已关闭");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    // 获取连接实例
    public JedisCluster getJedisCluster() {
        return jedisCluster;
    }
}

// 主节点A(槽位0-5000)
//   └─ 从节点A1(复制A的数据)
  
// 主节点B(槽位5001-10000)  
//   └─ 从节点B1(复制B的数据)
  
// 主节点C(槽位10001-16383)
//   └─ 从节点C1(复制C的数据)
Redis Cluster 的数据分布原理

每个服务不是全量的数据,只是每个里面存一部分

public class RedisClusterDataDistribution {
    public static void main(String[] args) {
        JedisCluster cluster = new JedisCluster(nodes);
        
        // 假设有3个主节点的Cluster:
        
        // 节点A (7000): 只存储 slot 0-5460 的数据
        cluster.set("key_in_slot_100", "value");    // slot 100 -> 节点A
        cluster.set("key_in_slot_3000", "value");   // slot 3000 -> 节点A
        
        // 节点B (7001): 只存储 slot 5461-10922 的数据  
        cluster.set("key_in_slot_6000", "value");   // slot 6000 -> 节点B
        cluster.set("key_in_slot_8000", "value");   // slot 8000 -> 节点B
        
        // 节点C (7002): 只存储 slot 10923-16383 的数据
        cluster.set("key_in_slot_12000", "value");  // slot 12000 -> 节点C
        cluster.set("key_in_slot_15000", "value");  // slot 15000 -> 节点C
        
        // 每个节点只有 1/3 的数据!
    }
}
数据分布可视化

Cluster 数据分布:

节点A (7000)       节点B (7001)       节点C (7002)
┌─────────────┐   ┌─────────────┐   ┌─────────────┐
│ Slot: 0     │   │ Slot: 5461  │   │ Slot: 10923 │
│   ...       │   │   ...       │   │   ...       │
│ Slot: 5460  │   │ Slot: 10922 │   │ Slot: 16383 │
│             │   │             │   │             │
│ 数据量: 33%  │   │ 数据量: 33%  │   │ 数据量: 33%  │
└─────────────┘   └─────────────┘   └─────────────┘

对比主从复制的数据分布:

主节点 (6379)       从节点1 (6380)       从节点2 (6381)
┌─────────────┐   ┌─────────────┐   ┌─────────────┐
│ 所有数据     │   │ 所有数据     │   │ 所有数据     │
│ 100%        │   │ 100%        │   │ 100%        │
│ User:1001   │   │ User:1001   │   │ User:1001   │
│ Order:2001  │   │ Order:2001  │   │ Order:2001  │  
│ Product:3001│   │ Product:3001│   │ Product:3001│
└─────────────┘   └─────────────┘   └─────────────┘
优缺点总结

Redis Cluster 的优点:

  • 超大容量:数据分片,突破单机内存限制
  • 写性能扩展:写入可以分散到多个节点
  • 读性能扩展:读取也可以分散到多个节点
  • 真正的水平扩展:可以轻松增加节点

Redis Cluster 的缺点:

  • 架构复杂:配置和维护更复杂
  • 跨slot操作限制:不支持跨节点的复杂操作
  • 客户端要求:需要支持Cluster的客户端
  • 数据迁移成本:扩容时需要数据重新分片

主从复制的优点:

  • 架构简单:配置和维护容易
  • 全量数据:每个节点都有完整数据
  • 操作灵活:支持所有Redis命令
  • 客户端兼容:任何Redis客户端都可以用

主从复制的缺点:

  • 容量限制:总容量受单节点内存限制
  • 写性能瓶颈:所有写操作都在主节点
  • 存储浪费:数据全量复制,存储效率低
选择建议

用 Redis Cluster 当:

  • 数据量 > 单机内存的70%
  • 需要真正的读写水平扩展
  • 业务可以接受跨slot操作限制

用 主从+哨兵 当:

  • 数据量 < 单机内存的70%
  • 主要是读多写少的场景
  • 需要简单的架构和运维
  • 需要使用所有Redis命令

哨兵配置

# 端口26379
port 26379

# 核心配置:监控名为mymaster的主节点,
# mymaster:给这个Redis集群起的名字,可以自定义
#192.168.43.212 6379:初始主节点的地址(哨兵启动时去这里找主节点)
#2:法定人数 - 至少需要2个哨兵同意才能判定主节点客观下线
sentinel monitor mymaster 192.168.43.212 6379 2
# 哨兵用这个密码去连接Redis主节点和从节点进行监控。
sentinel auth-pass mymaster 111111 # 主节点没密码的话,不用配置这个
# 主节点多少毫秒无响应后,哨兵将其标记为"主观下线"。
sentinel down-after-milliseconds mymaster 5000
# 故障转移的超时时间,如果在这个时间内没完成故障转移,会尝试新的故障转移。
sentinel failover-timeout mymaster 60000
# 故障转移后,同时向新主节点同步数据的从节点数量。
sentinel parallel-syncs mymaster 1

# 告诉其他哨兵"我自己的地址和端口",用于哨兵之间的自动发现
sentinel announce-ip 192.168.43.212
sentinel announce-port 26379
# 启动
cd Redis-x64-3.0.504 - Sentinel1
redis-server redis.windows.conf --sentinel

Redis 哨兵的自动恢复机制

// 初始状态
主节点A: slave-priority 50 (192.168.43.212)
从节点B: slave-priority 100 (192.168.43.213)  
从节点C: slave-priority 200 (192.168.43.214)

// 步骤1:主节点A宕机
主节点A宕机 → 哨兵开始故障转移
    ↓
比较从节点优先级:
- 节点B: priority 100
- 节点C: priority 200
    ↓
选择优先级更高的节点B(数字越小优先级越高!)
    ↓
节点B成为新主节点
    
// 步骤2:原主节点A恢复
原主节点A恢复启动
    ↓
哨兵检测到节点A在线
    ↓
发现集群已有主节点B
    ↓
强制节点A成为节点B的从节点
    ↓
节点A开始同步节点B的数据
    
// 注意:原主节点恢复后只会成为从节点,不会自动恢复成主节点。
// 1. 避免频繁的主节点切换
// 2. 保持系统稳定性:节点A:刚刚从故障中恢复,稳定性未知
// 3. 防止数据一致性问题

Jedis 内置的哨兵支持

代码是硬编码的,无法感知Redis主从的故障切换。当哨兵完成主从切换后,应用程序仍然在尝试连接旧的、已经宕机的主节点,导致服务不可用。

// 主节点连接池(写操作)
masterPool = new JedisPool(poolConfig, "127.0.0.1", 6379, 3000, "123456");

// 从节点连接池(读操作)
slavePools = new ArrayList<>();
slavePools.add(new JedisPool(poolConfig, "127.0.0.1", 6380, 3000, "123456"));
slavePools.add(new JedisPool(poolConfig, "127.0.0.1", 6381, 3000, "123456"));

// 我用这个来进行读写,假如6379宕机了,虽然哨兵选出了新的6380来使用,但是我这还是6379不能用

需要让应用程序能够动态地发现当前的主节点

核心思想: 应用程序连接的是哨兵集群,而不是具体的Redis节点。通过哨兵来查询当前有效的主节点地址。

// 例子1
public class RedisPoolUtils {

    private static Jedis jedis;

    private static JedisSentinelPool jedisSentinelPool;

    static{
        try {
            JedisPoolConfig config = new JedisPoolConfig();
            //最大空闲连接数, 默认8个
            config.setMaxIdle(8);
            //最大连接数, 默认8个
            config.setMaxTotal(8);
            //最小空闲连接数, 默认0
            config.setMinIdle(0);
            //获取连接时的最大等待毫秒数(如果设置为阻塞时BlockWhenExhausted),如果超时就抛异常, 小于零:阻塞不确定的时间,  默认-1
            config.setMaxWaitMillis(3000);
            //在获取连接的时候检查有效性,表示取出的redis对象可用, 默认false
            config.setTestOnBorrow(true);


            //redis服务器列表
            Set<String> sentinels = new HashSet<>();
            sentinels.add(new HostAndPort("192.168.43.212", 26379).toString());
            sentinels.add(new HostAndPort("192.168.43.213", 26379).toString());
            sentinels.add(new HostAndPort("192.168.43.214", 26379).toString());

            //初始化连接池
            jedisSentinelPool = new JedisSentinelPool("mymaster", sentinels, config, "111111");
            // 从池中获取一个Jedis对象
            jedis = jedisSentinelPool.getResource();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
// 例子2
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisSentinelPool;
import java.util.HashSet;
import java.util.Set;

public class RedisSentinelExample {

    private static JedisSentinelPool sentinelPool;

    static {
        // 1. 配置哨兵集群地址
        Set<String> sentinels = new HashSet<>();
        sentinels.add("127.0.0.1:26379");
        sentinels.add("127.0.0.1:26380");
        sentinels.add("127.0.0.1:26381"); // 添加所有哨兵节点

        // 2. 主节点名称 (在sentinel.conf中配置的 `mymaster`)
        String masterName = "mymaster";

        // 3. 创建基于哨兵的连接池
        sentinelPool = new JedisSentinelPool(masterName, sentinels, "123456");
    }

    // 写操作 - 自动获取当前主节点连接
    public void set(String key, String value) {
        try (Jedis jedis = sentinelPool.getResource()) {
            jedis.set(key, value);
        }
    }

    // 读操作 - 自动路由到从节点 (JedisSentinelPool内部处理)
    public String get(String key) {
        try (Jedis jedis = sentinelPool.getResource()) {
            return jedis.get(key);
        }
    }
}


工作原理:

  1. JedisSentinelPool 在初始化时,会连接你提供的哨兵节点。
  2. 通过哨兵查询当前 mymaster 对应的真正主节点IP和端口。
  3. 内部维护一个到当前主节点的连接池。
  4. 当哨兵进行主从切换时,JedisSentinelPool 能够自动感知到,并更新内部连接池到新的主节点。
  5. 对于读操作,默认也是从主节点读(保证强一致性),如果你希望从从节点读,需要额外的配置(见下文)。
扩展
  1. JedisSentinelPool + 配置实现读写分离,怎么个配置和使用问deepseek!

  2. 当然还可以自定义监听哨兵事件(更灵活,但复杂)。具体问deepseek!

redis 客户端查看信息

# 启动 连接主redis服务
cd Redis-x64-3.0.504 - Master
redis-cli -p 6379
# 查看主从信息
info replication

# 查看哨兵信息
# 连接哨兵
cd Redis-x64-3.0.504 - Sentinel1
redis-cli -p 26379
# 或者连接哨兵
redis-cli.exe -h 127.0.0.1 -p 26379

# 查看监控的主节点信息
127.0.0.1:26379> sentinel master mymaster
# 查看所有从节点信息
127.0.0.1:26379> sentinel slaves mymaster
# 查看其他哨兵节点信息
127.0.0.1:26379> sentinel sentinels mymaster
# 获取当前主节点地址
127.0.0.1:26379> sentinel get-master-addr-by-name mymaster
# 查看哨兵配置
127.0.0.1:26379> sentinel config mymaster

完整配置1主2从3哨兵流程

下载并解压

具体流程看:Redis - deyang - 博客园

下载并解压后得到:Redis-x64-3.0.504文件夹

image-20251119150511464

文件夹内容:

image-20251119150620447

复制1主2从3哨兵

复制6份Redis-x64-3.0.504,分别对应1主2从3哨兵

image-20251119150858599

配置

redis.windows.conf配置
# 所有Redis-x64-3.0.504里面的redis.windows.conf配置 以下配置追加到redis.windows.conf
# 主节点的无需修改,就用port 6379
# Master
port 6379
# Slave1
port 6380
slaveof 127.0.0.1 6379
# Slave2
port 6381
slaveof 127.0.0.1 6379

# sentinel1
port 26379
sentinel monitor mymaster 127.0.0.1 6379 2
# sentinel auth-pass mymaster 111111
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1
sentinel announce-ip 127.0.0.1
sentinel announce-port 26379

# sentinel2
port 26380
sentinel monitor mymaster 127.0.0.1 6379 2
# sentinel auth-pass mymaster 111111
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1
sentinel announce-ip 127.0.0.1
sentinel announce-port 26380

# sentinel3
port 26381
sentinel monitor mymaster 127.0.0.1 6379 2
# sentinel auth-pass mymaster 111111
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1
sentinel announce-ip 127.0.0.1
sentinel announce-port 26381
启动脚本
# master
cd Redis-x64-3.0.504 - Master
redis-cli -p 6379
# slave1
cd Redis-x64-3.0.504 - Slave1
redis-cli -p 6380
# sentinel
cd Redis-x64-3.0.504 - Sentinel1
redis-cli -p 26379

测试

# 启动对应客户端,查看对应信息
# 查看主从信息
info replication
# 查看主信息
sentinel master mymaster
# 查看所有从节点信息
sentinel slaves mymaster
# 查看其他哨兵节点信息
sentinel sentinels mymaster
# 获取当前主节点地址
sentinel get-master-addr-by-name mymaster
# 查看哨兵配置
sentinel config mymaster

# 校验:
# 1.主节点宕机,从节点自动变成主节点
# 2.原主节点恢复,自动变成从节点

# 主节点宕机测试,测试后会变成 6380变成主节点:
# 哨兵配置变化
port 26380
sentinel monitor mymaster 127.0.0.1 6380 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel config-epoch mymaster 3
sentinel leader-epoch mymaster 3
sentinel known-slave mymaster 127.0.0.1 6381
# 6379的配置变化
slaveof 127.0.0.1 6380
# 6380的slaveof配置被移除

# 配置变化的原因:这是因为哨兵在故障转移后,会将新的主节点信息持久化到其配置文件中,重启后它会依据这个最新的配置来初始化并管理集群
posted @ 2025-11-20 15:02  deyang  阅读(11)  评论(0)    收藏  举报