Redis入门学习教程,从入门到精通,Redis集群架构:语法知识点、使用方法与综合案例(6)

Redis集群架构:语法知识点、使用方法与综合案例

本章将深入探讨Redis的三种主要架构模式:主从复制(Master-Slave)哨兵机制(Sentinel)以及分片集群(Cluster) 。我们将重点讲解如何使用 Lettuce (Spring Data Redis的默认客户端)和 Spring Data Redis 进行整合,并涵盖节点的动态扩缩容及代理层(Predixy)的配置。


1. Redis 主从架构 (Master-Slave)

1.1 核心概念

  • 原理:一个主节点(Master)负责写操作,多个从节点(Slave)负责读操作和数据备份。数据单向同步(Master -> Slave)。
  • 特点:配置简单,但主节点故障需要人工干预切换,不具备自动高可用。

1.2 配置文件 (redis.conf)

Master 节点 : 无需特殊配置(默认即为Master)。
Slave 节点:

conf 复制代码
# 指定主节点地址
replicaof 192.168.1.100 6379
# 或者旧版本写法
# slaveof 192.168.1.100 6379

# 设置只读(默认就是只读,防止误写)
replica-read-only yes

1.3 Lettuce 整合主从架构

Lettuce 原生支持读写分离,通过 RedisClient 连接主节点,利用 StatefulRedisConnectionreadOnly() 方法切换到从节点读取。

案例代码:Lettuce 手动读写分离
java 复制代码
import io.lettuce.core.RedisClient;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import io.lettuce.core.protocol.ReadOnlyCommands;

public class LettuceMasterSlaveExample {
    public static void main(String[] args) {
        // 1. 创建客户端,通常连接主节点地址(也可以连接任意节点,Lettuce会自动发现拓扑,但在纯主从模式下建议连主)
        RedisClient client = RedisClient.create("redis://192.168.1.100:6379");

        // 2. 获取有状态连接
        StatefulRedisConnection<String, String> connection = client.connect();

        // --- 写操作 (必须在主节点) ---
        RedisCommands<String, String> syncCommands = connection.sync();
        syncCommands.set("user:1001", "ZhangSan");
        System.out.println("写入成功 (Master)");

        // --- 读操作 (切换到从节点) ---
        // 开启只读模式,后续命令会路由到从节点(如果客户端配置了多节点,Lettuce会自动负载均衡)
        connection.readOnly(); 
        
        String value = syncCommands.get("user:1001");
        System.out.println("读取结果 (Slave): " + value);

        // 关闭连接
        connection.close();
        client.shutdown();
    }
}

1.4 Spring Data Redis 整合主从架构

在Spring Boot中,主要通过配置 application.yml 指定主节点,并在代码中使用 @Readonly 注解或编程式设置实现读写分离。

配置文件 (application.yml)
yaml 复制代码
spring:
  data:
    redis:
      host: 192.168.1.100 # 主节点地址
      port: 6379
      password: your_password
      # 注意:Spring Data Redis 默认连接主节点。
      # 若要自动读写分离,通常需要配置多个节点或使用自定义配置类定义 LoadBalancer
      lettuce:
        pool:
          max-active: 8
案例代码:Spring Data Redis 读写分离
java 复制代码
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.ReadFrom; // 关键类

// 自定义配置类示例(启用读写分离策略)
@Configuration
public class RedisConfig {

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
        config.setHostName("192.168.1.100");
        config.setPort(6379);
        
        LettuceConnectionFactory factory = new LettuceConnectionFactory(config);
        
        // 【关键点】设置读取策略:从从节点读取
        // ReadFrom.REPLICA_PREFERRED: 优先从从节点读,如果没有则从主节点读
        factory.setReadFrom(ReadFrom.REPLICA_PREFERRED);
        
        return factory;
    }
    
    // 使用模板
    @Autowired
    private StringRedisTemplate redisTemplate;

    public void testReadWrite() {
        // 写操作 (自动路由到 Master)
        redisTemplate.opsForValue().set("order:1001", "Pending");
        
        // 读操作 (根据策略自动路由到 Slave)
        String status = redisTemplate.opsForValue().get("order:1001");
        System.out.println("订单状态: " + status);
    }
}

2. Redis 哨兵机制 (Sentinel)

2.1 核心概念

  • 原理:哨兵进程监控主从节点状态。当主节点宕机,哨兵集群投票选举新的主节点,并通知从节点切换。
  • 特点 :提供高可用(HA),自动故障转移。客户端连接的是哨兵地址,由哨兵告知当前主节点地址。

2.2 配置 Redis 哨兵集群 (sentinel.conf)

需要启动至少3个哨兵实例以达成多数派共识。

conf 复制代码
# 监控的主节点名称、IP、端口、法定人数(最少几个哨兵同意才切换)
sentinel monitor mymaster 192.168.1.100 6379 2

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

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

# 故障转移超时时间
sentinel failover-timeout mymaster 10000

# 并行同步的从节点数量
sentinel parallel-syncs mymaster 1

2.3 Lettuce 整合哨兵

Lettuce 通过 RedisSentinelClient 连接哨兵,自动感知主节点变化。

案例代码:Lettuce 连接哨兵
java 复制代码
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import io.lettuce.core.sentinel.api.StatefulRedisSentinelConnection;
import io.lettuce.core.sentinel.api.sync.RedisSentinelCommands;

import java.util.Arrays;

public class LettuceSentinelExample {
    public static void main(String[] args) {
        // 1. 构建哨兵地址列表
        RedisURI sentinelUri = RedisURI.builder()
                .withSentinel("192.168.1.201", 26379) // 哨兵1
                .withSentinel("192.168.1.202", 26379) // 哨兵2
                .withSentinel("192.168.1.203", 26379) // 哨兵3
                .withSentinelMasterId("mymaster")     // 监控的主节点名称
                .withPassword("your_password")
                .build();

        // 2. 创建客户端 (连接到哨兵)
        RedisClient client = RedisClient.create(sentinelUri);

        // 3. 获取连接 (客户端会自动从哨兵获取当前Master地址并连接)
        StatefulRedisConnection<String, String> connection = client.connect();
        RedisCommands<String, String> commands = connection.sync();

        // 4. 执行操作 (无需关心当前谁是Master,自动处理)
        commands.set("session:id", "abc-123");
        System.out.println("写入成功,当前主节点由哨兵自动定位");
        
        String val = commands.get("session:id");
        System.out.println("读取值: " + val);

        connection.close();
        client.shutdown();
    }
}

2.4 Spring Data Redis 整合哨兵

这是生产环境最常用的配置方式。

配置文件 (application.yml)
yaml 复制代码
spring:
  data:
    redis:
      sentinel:
        master: mymaster # 必须与 sentinel.conf 中的 monitor 名称一致
        nodes: 
          - 192.168.1.201:26379
          - 192.168.1.202:26379
          - 192.168.1.203:26379
      password: your_password
      lettuce:
        pool:
          max-active: 10
        # 可选:读取策略
        read-from: replica_preferred 
案例代码:Spring Boot 自动重连测试
java 复制代码
@Service
public class SentinelService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    public void saveData(String key, String value) {
        // 即使发生主从切换,这里也不需要修改代码
        // Lettuce 会收到哨兵通知并重新连接新的主节点
        try {
            redisTemplate.opsForValue().set(key, value);
            System.out.println("数据保存成功: " + key);
        } catch (Exception e) {
            System.err.println("写入失败,可能正在故障转移中: " + e.getMessage());
            // 实际生产中应增加重试机制
        }
    }
    
    public String getData(String key) {
        return redisTemplate.opsForValue().get(key);
    }
}

3. Redis Cluster (分片集群)

3.1 核心概念

  • 原理:去中心化,数据分片存储在16384个槽(Slots)中。每个节点负责一部分槽。
  • 特点:线性扩展,高可用(每个分片也是主从结构)。
  • 通信:节点间通过 Gossip 协议通信。

3.2 配置 Redis Cluster 服务

每个节点 redis.conf 需开启集群模式:

conf 复制代码
cluster-enabled yes
cluster-config-file nodes-6379.conf
cluster-node-timeout 5000

初始化集群命令 (使用 redis-cli):

bash 复制代码
# 创建3主3从集群
redis-cli --cluster create \
192.168.1.101:6379 192.168.1.102:6379 192.168.1.103:6379 \
192.168.1.104:6379 192.168.1.105:6379 192.168.1.106:6379 \
--cluster-replicas 1 -a your_password --cluster-yes

3.3 Lettuce 整合 Redis Cluster

Lettuce 对 Cluster 支持非常好,会自动处理 MOVEDASK 重定向。

案例代码:Lettuce Cluster 操作
java 复制代码
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.cluster.RedisClusterClient;
import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;
import io.lettuce.core.cluster.api.sync.RedisClusterCommands;

import java.util.Arrays;

public class LettuceClusterExample {
    public static void main(String[] args) {
        // 1. 只需提供一个种子节点地址,客户端会自动发现整个集群拓扑
        RedisURI uri = RedisURI.create("192.168.1.101", 6379);
        // 如果有密码
        // uri.setPassword("your_password"); 

        // 2. 创建 Cluster 客户端
        RedisClusterClient clusterClient = RedisClusterClient.create(uri);

        // 3. 获取连接
        StatefulRedisClusterConnection<String, String> connection = clusterClient.connect();
        RedisClusterCommands<String, String> commands = connection.sync();

        // 4. 操作数据
        // Lettuce 会自动计算 key 的 hash slot 并路由到正确的节点
        commands.set("cluster:key1", "value1");
        commands.set("cluster:key2", "value2");

        // 批量操作 (注意:MGET/MSET 在集群模式下要求所有 key 必须在同一个 slot,否则报错)
        // 解决方案:使用 {} 包裹哈希标签,强制路由到同一节点
        commands.mset(Map.of("{user}:1001:name", "Alice", "{user}:1001:age", "25"));

        System.out.println("Cluster 操作完成");
        
        connection.close();
        clusterClient.shutdown();
    }
}

3.4 Spring Data Redis 整合 Redis Cluster

配置文件 (application.yml)
yaml 复制代码
spring:
  data:
    redis:
      cluster:
        nodes:
          - 192.168.1.101:6379
          - 192.168.1.102:6379
          - 192.168.1.103:6379
          - 192.168.1.104:6379
          - 192.168.1.105:6379
          - 192.168.1.106:6379
        max-redirects: 3 # 最大重定向次数
      password: your_password
      lettuce:
        pool:
          enabled: true
        cluster:
          refresh:
            adaptive: true # 开启自适应拓扑刷新(重要!节点变动时自动更新路由)
            period: 60000  # 定期刷新间隔
案例代码:Spring Data Redis Cluster 注意事项
java 复制代码
@Service
public class ClusterService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    public void saveUserData(String userId, String name, String age) {
        // 【关键点】在集群模式下,涉及多个Key的操作(如 mset, transaction, lua脚本)
        // 必须保证这些Key落在同一个 Slot 上。
        // 使用 Hash Tag: {userId}
        
        String nameKey = "{" + userId + "}:name";
        String ageKey = "{" + userId + "}:age";

        Map<String, String> map = new HashMap<>();
        map.put(nameKey, name);
        map.put(ageKey, age);

        // 这样就能成功执行 mset,因为它们被路由到了同一个节点
        redisTemplate.opsForValue().multiSet(map);
    }
    
    public void nonCompliantOperation() {
        // 错误示范:以下代码在集群模式下会抛出 CrossSlotException
        // redisTemplate.opsForValue().multiSet(Map.of("key1", "v1", "key2", "v2")); 
        // 因为 key1 和 key2 很可能不在同一个槽
    }
}

3.5 动态追加/删除 Redis Cluster 数据节点

此操作通常通过命令行 redis-cli 完成,但也可以通过 Lettuce 编程实现(较少见,通常由运维脚本执行)。

场景 A:动态追加节点 (扩容)
  1. 启动新节点(配置好 cluster-enabled yes)。
  2. 将新节点加入集群。
  3. 分配槽(Slots)给新节点。
  4. 数据迁移。

命令行操作案例:

bash 复制代码
# 1. 将新节点 192.168.1.200:6379 加入现有集群 (接触任意现有节点)
redis-cli --cluster add-node 192.168.1.200:6379 192.168.1.101:6379 -a your_password

# 2. 检查集群状态,确认新节点已加入但无槽
redis-cli --cluster check 192.168.1.101:6379 -a your_password

# 3. 重新分片 (Resharding) - 将部分槽从旧节点移动到新节点
# 系统会交互式询问:移动多少个槽?目标节点ID?源节点ID?
redis-cli --cluster reshard 192.168.1.101:6379 -a your_password
场景 B:动态删除节点 (缩容)
  1. 迁移该节点上的所有槽到其他节点。
  2. 从集群中移除该节点。

命令行操作案例:

bash 复制代码
# 1. 先迁移槽 (假设要移除节点 ID 为 abc...def)
# 将该节点的所有槽移动到 192.168.1.101
redis-cli --cluster reshard 192.168.1.101:6379 --cluster-from <待删除节点ID> --cluster-to <接收节点ID> --cluster-slots <总槽数> -a your_password

# 2. 确认该节点槽数为 0 后,移除节点
redis-cli --cluster del-node 192.168.1.101:6379 <待删除节点ID> -a your_password

Java (Lettuce) 触发刷新拓扑 :

当节点变更后,客户端需要感知。如果开启了 adaptive: true (Spring配置),它会自动感知。否则可手动触发:

java 复制代码
// 获取集群拓扑刷新
clusterClient.getResources().getPartitions().reload();

4. Predixy 集群代理

4.1 核心概念

  • 作用:Predixy 是一个高性能的 Redis 集群代理。它对客户端屏蔽了集群的复杂性(如重定向、分片逻辑)。客户端只需连接 Predixy,就像连接单机 Redis 一样。
  • 优势:简化客户端代码,支持更复杂的路由策略,统一管理连接池。

4.2 Predixy 配置 (predixy.conf)

conf 复制代码
# 监听地址
bind 0.0.0.0:7100

# 后端 Redis Cluster 配置
cluster {
    name "my-redis-cluster"
    # 可以配置多个种子节点
    node 192.168.1.101:6379
    node 192.168.1.102:6379
    # 密码配置
    auth "your_password"
}

# 日志等其它配置...

4.3 客户端整合 (通用)

一旦部署了 Predixy,客户端(Lettuce 或 Spring Data Redis)的配置将退化为单机模式,指向 Predixy 的地址。

案例代码:通过 Predixy 访问集群
java 复制代码
// 此时完全不需要知道后端是集群还是哨兵
// 配置指向 Predixy 的端口 (例如 7100)
RedisClient client = RedisClient.create("redis://192.168.1.50:7100"); 
// 如果有密码
// RedisClient.create(RedisURI.create("redis://:your_password@192.168.1.50:7100"));

StatefulRedisConnection<String, String> conn = client.connect();
// 直接使用,无需处理 MOVED 异常,无需配置多个节点
conn.sync().set("proxy:key", "value_via_predixy");

Spring Boot 配置 (application.yml):

yaml 复制代码
spring:
  data:
    redis:
      host: 192.168.1.50 # Predixy IP
      port: 7100         # Predixy Port
      password: your_password
      # 不需要配置 cluster 或 sentinel 段落,当作单机使用

5. 综合性案例:高可用电商库存系统

场景描述

构建一个电商库存扣减服务。

  • 架构:采用 Redis Cluster (3主3从) 保证数据量和性能。
  • 高可用:结合哨兵思想(Cluster自带高可用),并使用 Spring Data Redis。
  • 需求
    1. 扣减库存必须是原子操作(使用 Lua 脚本)。
    2. 处理集群下的多键问题(商品库存 + 用户购买记录需在同一槽)。
    3. 模拟节点故障时的自动恢复。

5.1 依赖配置 (pom.xml)

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!--  Lombok for brevity -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

5.2 核心代码实现

A. Lua 脚本定义 (原子扣减)
java 复制代码
@Component
public class InventoryScript {

    // KEYS[1]: 商品库存 Key (e.g., {prod:1001}:stock)
    // KEYS[2]: 用户购买记录 Key (e.g., {prod:1001}:user:bought:u55)
    // ARGV[1]: 购买数量
    // 返回: 1 成功, 0 库存不足, -1 重复购买
    public static final String SCRIPT = 
        "local stock = tonumber(redis.call('GET', KEYS[1]) or '0') " +
        "local count = tonumber(ARGV[1]) " +
        "if stock < count then return 0 end " +
        "if redis.call('EXISTS', KEYS[2]) == 1 then return -1 end " +
        "redis.call('DECRBY', KEYS[1], count) " +
        "redis.call('SET', KEYS[2], '1', 'EX', '86400') " +
        "return 1";
}
B. 服务层实现
java 复制代码
@Service
@Slf4j
public class InventoryService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private RedisScript<Long> redisScript; // 需要配置 Bean

    /**
     * 扣减库存
     * @param productId 商品ID
     * @param userId 用户ID
     * @param count 数量
     * @return 0:失败(库存不足), 1:成功, -1:重复购买
     */
    public int deductStock(String productId, String userId, int count) {
        // 【关键】使用 Hash Tag 确保两个 Key 落在同一个 Slot,避免 CrossSlotException
        String hashTag = "{" + productId + "}";
        String stockKey = hashTag + ":stock";
        String userRecordKey = hashTag + ":user:bought:" + userId;

        try {
            // 执行 Lua 脚本
            Long result = redisTemplate.execute(
                DefaultRedisScript.of(InventoryScript.SCRIPT, Long.class),
                Arrays.asList(stockKey, userRecordKey),
                String.valueOf(count)
            );

            if (result == 1) {
                log.info("扣减成功: 商品{}, 用户{}", productId, userId);
                return 1;
            } else if (result == 0) {
                log.warn("库存不足: 商品{}", productId);
                return 0;
            } else {
                log.warn("重复购买: 商品{}, 用户{}", productId, userId);
                return -1;
            }
        } catch (Exception e) {
            log.error("Redis 执行异常,可能正在发生主从切换", e);
            // 在实际系统中,这里应触发重试机制 (Retry Template)
            throw new RuntimeException("库存服务暂时不可用", e);
        }
    }
    
    // 配置 RedisScript Bean
    @Bean
    public RedisScript<Long> redisScript() {
        return DefaultRedisScript.of(InventoryScript.SCRIPT, Long.class);
    }
}
C. 测试控制器 (模拟高并发与故障)
java 复制代码
@RestController
@RequestMapping("/api/inventory")
public class InventoryController {

    @Autowired
    private InventoryService inventoryService;

    @PostMapping("/buy")
    public ResponseEntity<String> buy(@RequestParam String prodId, 
                                      @RequestParam String userId, 
                                      @RequestParam int count) {
        int result = inventoryService.deductStock(prodId, userId, count);
        
        switch (result) {
            case 1: return ResponseEntity.ok("购买成功");
            case 0: return ResponseEntity.status(400).body("库存不足");
            case -1: return ResponseEntity.status(400).body("请勿重复购买");
            default: return ResponseEntity.status(500).body("系统繁忙");
        }
    }
}

5.3 综合案例总结

  1. 架构选择 :使用了 Redis Cluster 应对海量商品数据。
  2. 客户端配置 :Spring Boot 配置了 lettuce.cluster.refresh.adaptive=true,确保当某个分片的主节点宕机,哨兵机制完成切换后,应用能自动感知新拓扑,无需重启。
  3. 数据一致性 :利用 Lua 脚本 保证了"查库存"和"扣库存"的原子性。
  4. 集群规范 :巧妙使用 {} 哈希标签 解决了集群模式下多键操作的限制。
  5. 扩展性 :如果未来流量激增,可通过 redis-cli --cluster reshard 动态添加节点,代码无需任何修改。
相关推荐
xian_wwq2 小时前
【学习笔记】数据投毒的9种攻击方法与防御措施
笔记·学习·数据投毒
czlczl200209252 小时前
Redis过期删除策略
数据库·redis·缓存
tianyuanwo2 小时前
Koji 分布式编译调度机制深度解析:多架构异构节点的资源优化方案
分布式·架构
醇氧2 小时前
第一、二、三范式学习
数据库·学习·oracle
爱学习的小可爱卢3 小时前
Redis从入门到精通:入门到精通(万字详解)
数据库·redis·中间件
源远流长jerry3 小时前
RDMA 技术深度解析:从原理到实践
linux·网络·tcp/ip·架构·ip
Are_You_Okkk_3 小时前
开源知识库的核心技术赋能与企业级落地路径
人工智能·架构·开源
源远流长jerry3 小时前
RDMA 基本元素详解:从 WQE 到 QP 再到 CQ
linux·开发语言·网络·tcp/ip·架构·ip
苦瓜小生3 小时前
【黑马点评学习笔记 | 实战篇 】| 10-用户签到+UV统计
笔记·后端·学习