Redis-12-SpringBoot集成Redis哨兵模式(Lettuce或Redisson)
Redis哨兵的配置,参考我这篇文章:
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);
}
}
运行
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);
}
}
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信息,实际返回是以这个为准的。