Redis-12-SpringBoot集成Redis哨兵模式(Lettuce或Redisson)

Redis哨兵的配置,参考我这篇文章:

Redis-5-高可用

1.背景

网上搜半天没搜到份好用的,自己整理了下方便以后复制,基于springboot 2.6.13。

Jedis 、Lettuce 、Redisson都是Java中Redis的客户端,实际项目中,结合自己的需要引入。

Spring Boot 默认使用Lettuce作为Redis的客户端库。

特性/客户端 Jedis Lettuce Redisson
基本类型 阻塞 非阻塞 (异步/响应式) 非阻塞 (异步/响应式)
连接池 内置连接池 依赖于Netty的连接管理 依赖于Netty的连接管理
哨兵模式支持
集群模式支持
分布式锁
支持数据结构 基础数据结构 (字符串、列表、集合、哈希) 基础数据结构 (字符串、列表、集合、哈希) 支持高级数据结构 (RMap、RSet、RQueue等)
响应式编程支持
支持事务
断线重连 部分支持
支持Spring Data Redis
底层实现 直接基于Redis协议 基于Netty框架 基于Netty框架
序列化支持 自定义序列化 基于Spring的序列化机制 基于Spring的序列化机制
其他高级特性 分布式对象、锁、执行器、批处理、远程服务等
  • 需要一个简单、易用的Redis客户端,并且主要进行同步操作,Jedis是一个不错的选择。

  • 需要高并发和响应式编程支持,Lettuce更适合。

  • 需要利用Redis的高级特性,如分布式锁、高级数据结构等,Redisson是最佳选择。

2.Lettuce+哨兵模式

2.1 导入pom

        <!-- spring-redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- commons-pool2 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

2.2 编写配置文件

结合自己的项目更新,此处直接写在了application.yml中。

spring:
  redis:
    password: 主节点密码
    lettuce:
      pool:
        # 最大连接数
        max-active: 20
        # 连接池中获取连接时最大等待时间ms
        max-wait: 300
        # 最大空闲连接数
        max-idle: 5
        # 最小空闲连接数
        min-idle: 1
    sentinel:
      master: 要监控的主节点
      password: 哨兵密码
      # 哨兵集群
      nodes:
        - ip1:26379
        - ip2:26380
        - ip3:26381

2.3 编写配置实体类

RedisProperties:映射配置文件

package cn.yang37.za.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import java.util.Set;

/**
 * @description:
 * @class: RedisProperties
 * @author: yang37z@qq.com
 * @date: 2024/6/7 16:07
 * @version: 1.0
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {

    private String password;
    private Lettuce lettuce;
    private Sentinel sentinel;

    @Data
    public static class Lettuce {
        private Pool pool;

        @Data
        public static class Pool {
            private int maxActive;
            private int maxWait;
            private int maxIdle;
            private int minIdle;
        }
    }

    @Data
    public static class Sentinel {
        private String master;
        private String password;
        private Set<String> nodes;
    }
}

2.4 编写配置类

RedisSentinelConfig:哨兵配置信息加载、commonsPool配置信息加载、lettuce连接池构建、自定义RedisTemplate、自定义StringRedisTemplate

package cn.yang37.za.config;

import cn.yang37.za.properties.RedisProperties;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.lettuce.core.ReadFrom;
import io.lettuce.core.resource.DefaultClientResources;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
 * @description:
 * @class: RedisConfig
 * @author: yang37z@qq.com
 * @date: 2024/5/27 11:40
 * @version: 1.0
 */
@Slf4j
@Configuration
public class RedisConfig {

    @Resource
    private RedisProperties redisProperties;

    private DefaultClientResources clientResources;

    /**
     * 构建commonsPool配置
     *
     * @return .
     */
    @Bean
    public GenericObjectPoolConfig<?> genericObjectPoolConfig() {
        GenericObjectPoolConfig<?> config = new GenericObjectPoolConfig<>();
        config.setMinIdle(redisProperties.getLettuce().getPool().getMinIdle());
        config.setMaxIdle(redisProperties.getLettuce().getPool().getMaxIdle());
        config.setMaxTotal(redisProperties.getLettuce().getPool().getMaxActive());
        config.setMaxWaitMillis(redisProperties.getLettuce().getPool().getMaxWait());
        return config;
    }

    /**
     * 构建RedisSentinel配置
     *
     * @return .
     */
    @Bean
    public RedisSentinelConfiguration sentinelConfiguration() {
        RedisSentinelConfiguration redisSentinelConfiguration =
                new RedisSentinelConfiguration(redisProperties.getSentinel().getMaster(), redisProperties.getSentinel().getNodes());
        // 主节点密码
        redisSentinelConfiguration.setPassword(RedisPassword.of(redisProperties.getPassword()));
        // 哨兵密码
        redisSentinelConfiguration.setSentinelPassword(RedisPassword.of(redisProperties.getSentinel().getPassword().toCharArray()));
        return redisSentinelConfiguration;
    }

    /**
     * 构建lettuce连接池
     *
     * @param genericObjectPoolConfig .
     * @param sentinelConfiguration   .
     * @return .
     */
    @Bean
    public RedisConnectionFactory lettuceConnectionFactory(GenericObjectPoolConfig<?> genericObjectPoolConfig,
                                                           RedisSentinelConfiguration sentinelConfiguration) {
        // 声明资源
        this.clientResources = DefaultClientResources.create();

        // 构建lettuce配置
        LettucePoolingClientConfiguration lettuceClientConfiguration =
                LettucePoolingClientConfiguration.builder()
                        .poolConfig(genericObjectPoolConfig)
                        .readFrom(ReadFrom.REPLICA)
                        .clientResources(clientResources)
                        .build();

        // 构建lettuce连接池
        LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(sentinelConfiguration, lettuceClientConfiguration);
        // 每次获取连接时验证连接的有效性(注意性能影响)
        lettuceConnectionFactory.setValidateConnection(true);
        // 手动触发连接池初始化
        lettuceConnectionFactory.afterPropertiesSet();

        log.info("connected to redis sentinel,node info: {}", sentinelConfiguration.getSentinels());
        return lettuceConnectionFactory;
    }

    /**
     * 应用关闭时触发
     */
    @PreDestroy
    public void shutdown() {
        if (null != clientResources) {
            clientResources.shutdown(100, 100, TimeUnit.MILLISECONDS);
        }
    }

    /**
     * 1.使用自定义lettuce连接池
     * 2.声明序列化方式
     *
     * @param connectionFactory .
     * @return .
     */
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(@Qualifier("lettuceConnectionFactory") RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);

        // 配置String的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // 配置Json的序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // Key采用String的序列化方式
        redisTemplate.setKeySerializer(stringRedisSerializer);
        // Hash的key也采用String的序列化方式
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        // Value序列化方式采用jackson
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // Hash的value序列化方式采用jackson
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    /**
     * 1.使用自定义lettuce连接池
     * 2.声明序列化方式
     *
     * @param connectionFactory .
     * @return .
     */
    @Bean
    public StringRedisTemplate stringRedisTemplate(@Qualifier("lettuceConnectionFactory") RedisConnectionFactory connectionFactory) {
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        stringRedisTemplate.setConnectionFactory(connectionFactory);
        stringRedisTemplate.setKeySerializer(stringRedisSerializer);
        stringRedisTemplate.setValueSerializer(stringRedisSerializer);

        stringRedisTemplate.afterPropertiesSet();
        return stringRedisTemplate;
    }

}

注意,读取的策略根据ReadFrom来配置。

        // 构建lettuce配置
        LettucePoolingClientConfiguration lettuceClientConfiguration =
                LettucePoolingClientConfiguration.builder()
                        .poolConfig(genericObjectPoolConfig)
                        // 确定从哪个节点读取数据
                        .readFrom(ReadFrom.REPLICA)
                        .clientResources(clientResources)
                        .build();
策略 用途
MASTER / UPSTREAM 从主节点读取数据。主节点通常处理所有写操作。
MASTER_PREFERRED / UPSTREAM_PREFERRED 优先从主节点读取数据,但如果主节点不可用,则从从节点读取数据。
REPLICA_PREFERRED 优先从从节点读取数据,但如果所有从节点不可用,则从主节点读取数据。
REPLICA 仅从从节点读取数据。
SLAVE (Deprecated) 仅从从节点读取数据。已废弃,用 REPLICA 替代。
LOWEST_LATENCY 从具有最低延迟的节点读取数据,可能是主节点或从节点。
NEAREST (Deprecated) 从最近的节点读取数据。已废弃,通常 LOWEST_LATENCY 替代。
ANY 从任意节点读取数据。通常用于负载均衡。
ANY_REPLICA 从任意从节点读取数据。用于从节点负载均衡,但不包括主节点。

2.5 执行测试

导入test组件

        <!-- test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

编写RedisControl的测试用例

package cn.yang37.za.controller;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Slf4j
@SpringBootTest
class RedisControllerTest {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Test
    void name1() {
        final String key = "yang37";

        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "123", 600, TimeUnit.SECONDS);
        log.info("flag: {}", flag);

        String value = stringRedisTemplate.opsForValue().get(key);
        log.info("value: {}", value);

        Long expire = stringRedisTemplate.getExpire(key, TimeUnit.SECONDS);
        log.info("expire: {}", expire);
    }

}

运行

image-20240607193558304

3.Redisson+哨兵模式

3.1 导入pom

        <!-- spring-redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- Redisson -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.18.0</version>
        </dependency>

3.2 编写配置文件

src/main/resources目录下新建一个名为redisson.yaml的文件。

sentinelServersConfig:
  masterName: "mymaster"
  sentinelAddresses:
    - "redis://175.178.189.195:26379"
    - "redis://175.178.189.195:26380"
    - "redis://175.178.189.195:26381"
  password: "yl123456"
  sentinelPassword: "yl123456"
  timeout: 3000

3.3 编写配置类

package cn.yang37.za.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.spring.data.connection.RedissonConnectionFactory;
import org.springframework.beans.factory.annotation.Qualifier;
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.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.io.IOException;

/**
 * @description:
 * @class: RedissonConfig
 * @author: yang37z@qq.com
 * @date: 2024/6/7 18:16
 * @version: 1.0
 */
@Configuration
public class RedisConfig {

    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient() throws IOException {
        Config config = Config.fromYAML(RedisConfig.class.getClassLoader().getResource("redisson.yaml"));
        return Redisson.create(config);
    }

    @Bean
    public RedisConnectionFactory redissonConnectionFactory(RedissonClient redissonClient) {
        return new RedissonConnectionFactory(redissonClient);
    }

    /**
     * 1.使用redisson
     * 2.声明序列化方式
     *
     * @param connectionFactory .
     * @return .
     */
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(@Qualifier("redissonConnectionFactory") RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);

        // 配置String的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // 配置Json的序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // Key采用String的序列化方式
        redisTemplate.setKeySerializer(stringRedisSerializer);
        // Hash的key也采用String的序列化方式
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        // Value序列化方式采用jackson
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // Hash的value序列化方式采用jackson
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    /**
     * 1.使用redisson
     * 2.声明序列化方式
     *
     * @param connectionFactory .
     * @return .
     */
    @Bean
    public StringRedisTemplate stringRedisTemplate(@Qualifier("redissonConnectionFactory") RedisConnectionFactory connectionFactory) {
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        stringRedisTemplate.setConnectionFactory(connectionFactory);
        stringRedisTemplate.setKeySerializer(stringRedisSerializer);
        stringRedisTemplate.setValueSerializer(stringRedisSerializer);

        stringRedisTemplate.afterPropertiesSet();
        return stringRedisTemplate;
    }

}

3.4 测试

package cn.yang37.za.controller;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.redisson.api.RedissonClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Slf4j
@SpringBootTest
class RedisControllerTest {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 锁的场景 可以直接使用redissonClient
     */
    @Resource
    private RedissonClient redissonClient;

    @Test
    void name1() {
        final String key = "yang37";

        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "123", 600, TimeUnit.SECONDS);
        log.info("flag: {}", flag);

        String value = stringRedisTemplate.opsForValue().get(key);
        log.info("value: {}", value);

        Long expire = stringRedisTemplate.getExpire(key, TimeUnit.SECONDS);
        log.info("expire: {}", expire);
    }

}

image-20240607192742848

4.常见问题

4.1 从节点或者哨兵总是取不到正确的ip

因为我是自己测试,一个1主2从3哨兵的配置,各个配置中用的ip都是127.0.0.1。

类型 端口 配置文件 说明
master 6379 redis1.conf redis主节点
slave1 6380 redis2.conf redis从节点1
slave2 6381 redis3.conf redis从节点2
sentinel1 26379 sentinel1.conf 哨兵1
sentinel2 26380 sentinel2.conf 哨兵2
sentinel3 26381 sentinel3.conf 哨兵3

一开始,在编写Lettuce那节的时候,项目能启动,但是运行起来后,显示从节点一直去访问127.0.0.1,通过修改sentinel monitor中的ip解决了。

sentinel monitor mymaster 外网地址 6379 2

切换到Redisson,又发现哨兵节点一直在连接内网地址,直接连接不上哨兵,启动都报错了。

后面发现有两个更优的配置项,可以直接声明对外的ip,容器环境同样好用。

  • 从节点配置文件
# 声明ip和端口
slave-announce-ip 声明ip
slave-announce-port 声明端口
  • 哨兵配置文件
# 声明ip和端口
sentinel announce-ip 声明ip
sentinel announce-port 声明端口

最终,你要检查写你redis给你哨兵配置中自动追加的ip信息,实际返回是以这个为准的。

image-20240607191348178

posted @ 2024-06-07 17:22  羊37  阅读(666)  评论(0编辑  收藏  举报