Redis集群搭建

[!tip]

本文主要讲解Redis集群搭建以及在Java中如何与Spring Boot项目进行整合。

同时,因为涉及到集群,要起多个Redis实例,我没有这么多机器,所以就统一用的云应用做的。

我使用的云应用是雨云这家的,点击此处推广链接直达,且有优惠。

在正式开始之前,Redis的集群搭建有多种方式,你可以是同一内网下的,也可以是跨网的,可以是直接使用二进制包的方式搭建,也可以直接使用Docker搭建。

我这里是直接使用的云应用(k8s+Docker)来完成的。这里无论那种方式,核心原理都是一样的。

1. Redis配置文件

当你把Redis安装好了之后,再正式启动之前,你需要修改redis.conf文件。

核心修改的配置如下:

# 让任意IP可以连接本Redis
bind 0.0.0.0 

# 禁用保护模式 根据bind配置视情况而定,不强制要求
protected-mode no 

# redis是否以守护进程运行,注意,如果你的redis是docker启动的,那么这里必须是no
daemonize yes

# 配置redis的连接密码,不强求配置,但是最后还是配上
requirepass 你的redis密码 

# 配置redis集群密码,当从节点连接主节点的时候,需要使用到此密码,所以建议不论是从节点还是主节点,都配置上,且密码保持一致
# 个人建议此配置需要强制配置
masterauth redis集群密码 

# 开启集群模式,强制要求
cluster-enabled yes

# 指定集群节点配置文件露肩,强制要求,注意,这里的文件不能重名,也就是说如果你是同一台机器启动的多个redis实例,
# 那么这里可能需要更改
cluster-config-file nodes-6379.conf

# 定义集群节点间通信的超时时间。如果主节点挂了,那么从节点在超过这个时间之后仍然无法接受到主节点的数据,
# 那么就会判断主节点已经失效了,此时从节点有可能会被选举为新的主节点
cluster-node-timeout 15000

# 由于你的redis要配置为集群,在集群模式下,只有1个数据库,所以配置为1
# 此处不强制要求,如果没配置,redis日志中只是有warning日志
databases 1

# 在复杂网络环境(如Docker、NAT、云服务器、负载均衡后)中正确暴露集群节点地址,
# 这里建议配置上,否则后面java程序连接的时候,他会默认走内网IP,你不一定能够连上
cluster-announce-ip IP地址(公网/内网)

# 节点对外宣告的客户端端口,一般和redis客户端的端口保持一致
cluster-announce-port 6379

# 节点对外宣告的集群总线端口,一般是redis客户端的端口+10000
cluster-announce-bus-port 16379

[!important]

对于redis的端口和集群总线端口最好要保证各个redis能够访问,也就是防火墙你得放过这些端口,因为搭建集群的时候,这两个端口都是必须的。

我这里是云应用(k8s+Docker),所以我的这里的需要的配置如下:

bind 0.0.0.0 
protected-mode no 
daemonize no
requirepass 1q2w#E
masterauth 1q2w#E
cluster-enabled yes
# 由于是docker启动的,每个docker启动一个redis,所以我这里不需要改这里的文件名字,不会出现冲突
cluster-config-file nodes-6379.conf
cluster-node-timeout 15000
databases 1
cluster-announce-ip 公网IP
cluster-announce-port 6379
cluster-announce-bus-port 16379

2. 端口开放(可选)

要保证集群内能够顺利通信,必须开放集群总线端口,同时,如果你要外部可访问你的redis,你还要把redis的客户端端口给开放出来,也就是常说的6379(根据实际开放)。

我这里是云应用(k8s+Docker),我要配置k8s的service将这两个端口开放出来,如图:

service开放redis客户端端口与集群总线端口

[!note]

不论你是以何种方式开放端口,只要开放出来就行了。

3. Redis集群搭建

3.1 主节点搭建

配置文件以及端口都准备好了之后,现在我们就可以搭建集群了,对于Redis集群,至少要有3个节点才可搭建,如果你只有2个Redis节点,那么只能搭建主从,不能搭建集群。

所以,我这里准备3个Redis,3个Redis的配置文件都使用上述的配置文件,使用命令搭建Redis集群:

redis-cli -a redis密码 --cluster create \
  redis主节点地址(IP):客户端端口 \
  redis主节点地址(IP):客户端端口 \
  redis主节点地址(IP):客户端端口 \
  ... \ # 其余多个master节点
  --cluster-replicas 0

对应到我这里,我的命令就是如下:

redis-cli -a "1q2w#E" --cluster create \
  svc-redis-wkii6w:6379 \
  svc-redis-7ddieo:6379 \
  svc-redis-cnmlth:6379 \
  --cluster-replicas 0

[!tip]

由于我这里是k8s,所以我使用的是service+端口的方式访问,这样在k8s中,多个Pod在内网就能够互相通信了。

主节点搭建

主节点搭建

[!note]

如果在执行上述命令的时候,在执行过程中,如果一直卡在Sending CLUSTER MEET messages to join the cluster Waiting for the cluster to join。这一步,那么基本上就是你的集群总线端口没有开放出来。

当上述命令执行成功之后,3个master节点就搭建好了。

在任意节点执行如下命令查看验证:

redis-cli -a "1q2w#E" cluster nodes

主节点搭建成功

3.2 从节点加入到主节点中

之前构建了集群的主节点(Masrer节点),如果你还要搭建从节点(Slave节点),并且把从节点也加入到集群中,那么这时候就构成了主从集群,容灾效果更好,并且从节点负责读,主节点负责写,读写也是分离的,性能更好。

对于从节点的配置文件更改,你参考主节点的配置文件就可以了,唯一要注意的是必须要配置masterauth,这样从节点才能够连接上主节点,从而进行通信。

同理,从节点也是需要开放客户端端口和集群总线端口,然后通过命令将改节点作为从节点加入到主节点中:

redis-cli -a redis密码 --cluster add-node 从节点地址(IP):客户端端口 主节点地址(IP):客户端端口 --cluster-slave

对应到我这里就是:

redis-cli -a 1q2w#E --cluster add-node svc-redis-0nabc7:6379 svc-redis-wkii6w:6379 --cluster-slave

我的svc-redis-0nabc7就是我的从节点Redis的地址,我要把这个Redis节点当作从节点加入到主节点svc-redis-wkii6w中,从而构成主从结构,这里需要在对应的主节点上面执行该命令:

从节点加入集群成功

集群节点状态

3.3 连接工具连接集群(可选)

当你的集群搭建好了之后,你就可以使用Redis可视化工具连接集群,我这里的使用AnotherRedisDesktopManager作为连接工具。

连接配置

4. SpringBoot整合Redis集群与Redisson

添加必要依赖:

<!--Redis与SpringBoot的整合依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- Redisson与SpringBoot的整合依赖 -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.52.0</version>
</dependency>

编写配置文件:

server:
  port: 12344

spring:
  application:
    name: redis-cluster-demo
  redis:
    # Redis 集群配置
    cluster:
      nodes:
        - 114.66.61.131:6379
        - 114.66.61.130:6379
        - 110.42.45.237:6379
        - 110.42.45.236:6379
      max-redirects: 3 # 最大重定向次数
    password: 1q2w#E # 如果有密码
    connect-timeout: 3s # 连接超时时间
    timeout: 3s # 读取超时时间

    # Lettuce 连接池配置
    lettuce:
      pool:
        max-active: 200 # 连接池最大连接数
        max-idle: 10 # 连接池最大空闲连接数
        min-idle: 0 # 连接池最小空闲连接数
        max-wait: -1ms # 连接池最大阻塞等待时间
      cluster:
        refresh:
          period: 30s # 集群拓扑刷新周期
          adaptive: true # 自适应刷新策略

编写Redis与Redisson的配置类。

Redis配置类:

package cn.yunrain.redisclusterdemo.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * Author 念心卓
 * Date 2025/12/2
 * Description Redis配置类
 */
@Configuration
@EnableCaching // 启用缓存支持,后续可以使用Spring Cache注解
public class RedisConfig {
    /**
     * 自定义RedisTemplate,设置序列化方式,避免存储乱码
     *
     * @param redisConnectionFactory Redis连接工厂
     * @return 自定义的RedisTemplate
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        // 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);

        ObjectMapper mapper = new ObjectMapper();

        // 设置可见性和类型信息,针对所有属性(字段、getter、setter等),允许所有访问级别的属性都被序列化(包括private私有字段)解决反序列化时的类型丢失问题
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 启用默认的类型信息,在JSON中添加类型信息(如"@class":"com.example.User"),这样反序列化时才能知道要将JSON还原成什么类型的Java对象
        // 注意:使用LaissezFaireSubTypeValidator来避免安全风险,DefaultTyping.NON_FINAL:为非final类型(绝大多数类)添加类型信息
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);

        // 使用 StringRedisSerializer 来序列化和反序列化 redis 的 key 值
        StringRedisSerializer stringSerializer = new StringRedisSerializer();

        // key 采用 String 的序列化方式
        template.setKeySerializer(stringSerializer);
        // hash 的 key 也采用 String 的序列化方式
        template.setHashKeySerializer(stringSerializer);
        // value 序列化方式采用 jackson
        template.setValueSerializer(serializer);
        // hash 的 value 序列化方式采用 jackson
        template.setHashValueSerializer(serializer);

        // 初始化Template,检查必要属性是否已设置。这个方法由Spring自动调用,这里显式调用确保立即初始化。
        template.afterPropertiesSet();
        return template;
    }
}

Redisson配置类:

package cn.yunrain.redisclusterdemo.config;

import cn.yunrain.redisclusterdemo.config.properties.RedisProperties;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.codec.JsonJacksonCodec;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

/**
 * Author 念心卓
 * Date 2025/12/2
 * Description Redisson配置类(集群模式)
 */
@Configuration
public class RedissonConfig {
    @Resource
    private RedisProperties redisProperties;

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();

        String[] clusterNodes = redisProperties.getCluster().getNodes();

        // 配置集群模式
        String[] nodes = new String[clusterNodes.length];
        for (int i = 0; i < clusterNodes.length; i++) {
            nodes[i] = "redis://" + clusterNodes[i];
        }

        config.useClusterServers() // 使用集群服务器模式
                .addNodeAddress(nodes) // 添加集群节点地址
                .setPassword(redisProperties.getPassword().isEmpty() ? null : redisProperties.getPassword()) // 设置密码,如果为空则不设置
                .setScanInterval(2000) // 集群扫描间隔时间
                .setConnectTimeout(3000) // 连接超时
                .setTimeout(3000) // 命令等待超时
                .setRetryAttempts(3) // 命令失败重试次数
                .setRetryInterval(1500) // 命令重试间隔
                .setMasterConnectionPoolSize(64) // 主节点连接池大小,针对写操作
                .setSlaveConnectionPoolSize(64) // 从节点连接池大小,针对读操作
                .setIdleConnectionTimeout(10000); // 连接空闲超时,超过此时间未使用的连接将被关闭

        // 设置编码,使用JsonJacksonCodec进行序列化和反序列化
        config.setCodec(new JsonJacksonCodec());

        // 设置线程数
        config.setThreads(16);
        config.setNettyThreads(32);

        // 设置传输模式为NIO
        config.setTransportMode(org.redisson.config.TransportMode.NIO);

        return Redisson.create(config);
    }
}

由于spring.redis.cluster.nodes的配置值是一个数组,所以,我这里单独写了一个RedisProperties类,来获取数组:

package cn.yunrain.redisclusterdemo.config.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * Author 念心卓
 * Date 2025/12/2
 * Description Redis配置
 */
@ConfigurationProperties(prefix = "spring.redis")
@Data
@Component
public class RedisProperties {
    private Cluster cluster;
    private String password;

    @Data
    public static class Cluster {
        private String[] nodes;
        private Integer maxRedirects;
    }
}

现在启动程序验证是否能够成功启动:

连接成功并成功启动程序

[!caution]

如果你的Redis和我一样是Docker/Nat/k8s/云应用搭建的,并且此处发送是内网IP,同时,你程序也连接不上,那么你就需要检查一下你每个Redis的redis.conf配置文件中,对应的cluster-announce-xxx配置是否正确。