SpringDataRedis 支持IPV6
- 项目需求
- 适配IPV6的哨兵模式
项目需求
1、应用程序支持单机redis的ipv4和ipv6连接
2、应用程序支持哨兵模式的redis的ipv4和ipv6的连接
适配IPV6的哨兵模式
1、配置文件
spring:
redis:
sentinel:
master: mymaster
nodes:
- "[2400:379:1200::30]:15610"
- "[2400:379:1200::31]:15610"
- "[2400:379:1200::32]:15610"
password: 123456
timeout: 10000
lettuce:
pool:
max-active: 10000
max-idle: 200
min-idle: 100
max-wait: 2000
说明:①、ipv6的地址需要将按照图示格式来,不然会报yaml解析错误
②、解析不报错了,需要重写连接
2、错误堆栈
2、springdataredis不支持ipv6的核心代码
private List<RedisNode> createSentinels(RedisProperties.Sentinel sentinel) {
List<RedisNode> nodes = new ArrayList<>();
for (String node : sentinel.getNodes()) {
try {
// ipv6不支持
String[] parts = StringUtils.split(node, ":");
Assert.state(parts.length == 2, "Must be defined as 'host:port'");
nodes.add(new RedisNode(parts[0], Integer.parseInt(parts[1])));
}
catch (RuntimeException ex) {
throw new IllegalStateException("Invalid redis sentinel property '" + node + "'", ex);
}
}
return nodes;
}
3、解决支持IPV6哨兵模式
网络上大部分是自定义配置信息,然后重写redis连接。此处为了适配我们的自身需求,想通过修改源码的形式来解决。核心就是修改createSentinels方法里面的逻辑。分割IPV6格式,用com.google.common.net.HostAndPort.
package com.tongtech.common.redis;
import com.google.common.net.HostAndPort;
import io.lettuce.core.ClientOptions;
import io.lettuce.core.SocketOptions;
import io.lettuce.core.TimeoutOptions;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import io.lettuce.core.resource.ClientResources;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.LettuceClientConfigurationBuilderCustomizer;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.*;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* @author lhc
* @version 1.0
* @description 自定义Redis配置
* @date 2022/8/11 17:02
*/
@Configuration
public class RedisConfiguration {
@Autowired
RedisProperties properties;
@Bean
@ConditionalOnMissingBean(RedisConnectionFactory.class)
LettuceConnectionFactory redisConnectionFactory(
ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,
ClientResources clientResources) {
LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(builderCustomizers, clientResources,
properties.getLettuce().getPool());
return createLettuceConnectionFactory(clientConfig);
}
private LettuceConnectionFactory createLettuceConnectionFactory(LettuceClientConfiguration clientConfiguration) {
// 哨兵
RedisProperties.Sentinel sentinelProperties =properties.getSentinel();
if (properties.getSentinel() != null) {
return new LettuceConnectionFactory(getSentinelConfig(), clientConfiguration);
}
// cluster
if (properties.getCluster() != null) {
return new LettuceConnectionFactory(getClusterConfiguration(), clientConfiguration);
}
// 单机
return new LettuceConnectionFactory(getStandaloneConfig(), clientConfiguration);
}
protected final RedisSentinelConfiguration getSentinelConfig() {
RedisProperties.Sentinel sentinelProperties = this.properties.getSentinel();
if (sentinelProperties != null) {
RedisSentinelConfiguration config = new RedisSentinelConfiguration();
config.master(sentinelProperties.getMaster());
config.setSentinels(createSentinels(sentinelProperties));
config.setUsername(this.properties.getUsername());
if (this.properties.getPassword() != null) {
config.setPassword(RedisPassword.of(this.properties.getPassword()));
}
if (sentinelProperties.getPassword() != null) {
config.setSentinelPassword(RedisPassword.of(sentinelProperties.getPassword()));
}
config.setDatabase(this.properties.getDatabase());
return config;
}
return null;
}
/**
* Create a {@link RedisClusterConfiguration} if necessary.
* @return {@literal null} if no cluster settings are set.
*/
protected final RedisClusterConfiguration getClusterConfiguration() {
if (this.properties.getCluster() == null) {
return null;
}
RedisProperties.Cluster clusterProperties = this.properties.getCluster();
RedisClusterConfiguration config = new RedisClusterConfiguration(clusterProperties.getNodes());
if (clusterProperties.getMaxRedirects() != null) {
config.setMaxRedirects(clusterProperties.getMaxRedirects());
}
config.setUsername(this.properties.getUsername());
if (this.properties.getPassword() != null) {
config.setPassword(RedisPassword.of(this.properties.getPassword()));
}
return config;
}
private List<RedisNode> createSentinels(RedisProperties.Sentinel sentinel) {
List<RedisNode> nodes = new ArrayList<>();
for (String node : sentinel.getNodes()) {
try {
// 修改原有方法 支持ipv6
HostAndPort hostAndPort = HostAndPort.fromString(node);
String host = hostAndPort.getHost();
int port = hostAndPort.getPort();
Assert.state(StringUtils.hasLength(host), "Must be defined as 'host:port'");
nodes.add(new RedisNode(host, port));
// String[] parts = StringUtils.split(node, ":");
// Assert.state(parts.length == 2, "Must be defined as 'host:port'");
// nodes.add(new RedisNode(parts[0], Integer.parseInt(parts[1])));
}
catch (RuntimeException ex) {
throw new IllegalStateException("Invalid redis sentinel property '" + node + "'", ex);
}
}
return nodes;
}
protected final RedisStandaloneConfiguration getStandaloneConfig() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
if (StringUtils.hasText(this.properties.getUrl())) {
RedisConfiguration.ConnectionInfo connectionInfo = parseUrl(this.properties.getUrl());
config.setHostName(connectionInfo.getHostName());
config.setPort(connectionInfo.getPort());
config.setUsername(connectionInfo.getUsername());
config.setPassword(RedisPassword.of(connectionInfo.getPassword()));
}
else {
config.setHostName(this.properties.getHost());
config.setPort(this.properties.getPort());
config.setUsername(this.properties.getUsername());
config.setPassword(RedisPassword.of(this.properties.getPassword()));
}
config.setDatabase(this.properties.getDatabase());
return config;
}
protected RedisConfiguration.ConnectionInfo parseUrl(String url) {
try {
URI uri = new URI(url);
String scheme = uri.getScheme();
if (!"redis".equals(scheme) && !"rediss".equals(scheme)) {
throw new RedisUrlSyntaxException(url);
}
boolean useSsl = ("rediss".equals(scheme));
String username = null;
String password = null;
if (uri.getUserInfo() != null) {
String candidate = uri.getUserInfo();
int index = candidate.indexOf(':');
if (index >= 0) {
username = candidate.substring(0, index);
password = candidate.substring(index + 1);
}
else {
password = candidate;
}
}
return new RedisConfiguration.ConnectionInfo(uri, useSsl, username, password);
}
catch (URISyntaxException ex) {
throw new RedisUrlSyntaxException(url, ex);
}
}
static class ConnectionInfo {
private final URI uri;
private final boolean useSsl;
private final String username;
private final String password;
ConnectionInfo(URI uri, boolean useSsl, String username, String password) {
this.uri = uri;
this.useSsl = useSsl;
this.username = username;
this.password = password;
}
boolean isUseSsl() {
return this.useSsl;
}
String getHostName() {
return this.uri.getHost();
}
int getPort() {
return this.uri.getPort();
}
String getUsername() {
return this.username;
}
String getPassword() {
return this.password;
}
}
private LettuceClientConfiguration getLettuceClientConfiguration(
ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,
ClientResources clientResources, RedisProperties.Pool pool) {
LettuceClientConfiguration.LettuceClientConfigurationBuilder builder = createBuilder(pool);
applyProperties(builder);
if (StringUtils.hasText(properties.getUrl())) {
customizeConfigurationFromUrl(builder);
}
builder.clientOptions(createClientOptions());
builder.clientResources(clientResources);
builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder.build();
}
private LettuceClientConfiguration.LettuceClientConfigurationBuilder applyProperties(
LettuceClientConfiguration.LettuceClientConfigurationBuilder builder) {
if (properties.isSsl()) {
builder.useSsl();
}
if (properties.getTimeout() != null) {
builder.commandTimeout(properties.getTimeout());
}
if (properties.getLettuce() != null) {
RedisProperties.Lettuce lettuce = properties.getLettuce();
if (lettuce.getShutdownTimeout() != null && !lettuce.getShutdownTimeout().isZero()) {
builder.shutdownTimeout(properties.getLettuce().getShutdownTimeout());
}
}
if (StringUtils.hasText(properties.getClientName())) {
builder.clientName(properties.getClientName());
}
return builder;
}
private ClientOptions createClientOptions() {
ClientOptions.Builder builder = initializeClientOptionsBuilder();
Duration connectTimeout = properties.getConnectTimeout();
if (connectTimeout != null) {
builder.socketOptions(SocketOptions.builder().connectTimeout(connectTimeout).build());
}
return builder.timeoutOptions(TimeoutOptions.enabled()).build();
}
private LettuceClientConfiguration.LettuceClientConfigurationBuilder createBuilder(RedisProperties.Pool pool) {
if (pool == null) {
return LettuceClientConfiguration.builder();
}
return new PoolBuilderFactory().createBuilder(pool);
}
private ClientOptions.Builder initializeClientOptionsBuilder() {
if (properties.getCluster() != null) {
ClusterClientOptions.Builder builder = ClusterClientOptions.builder();
RedisProperties.Lettuce.Cluster.Refresh refreshProperties = properties.getLettuce().getCluster().getRefresh();
ClusterTopologyRefreshOptions.Builder refreshBuilder = ClusterTopologyRefreshOptions.builder()
.dynamicRefreshSources(refreshProperties.isDynamicRefreshSources());
if (refreshProperties.getPeriod() != null) {
refreshBuilder.enablePeriodicRefresh(refreshProperties.getPeriod());
}
if (refreshProperties.isAdaptive()) {
refreshBuilder.enableAllAdaptiveRefreshTriggers();
}
return builder.topologyRefreshOptions(refreshBuilder.build());
}
return ClientOptions.builder();
}
private void customizeConfigurationFromUrl(LettuceClientConfiguration.LettuceClientConfigurationBuilder builder) {
RedisConfiguration.ConnectionInfo connectionInfo = parseUrl(properties.getUrl());
if (connectionInfo.isUseSsl()) {
builder.useSsl();
}
}
/**
* Inner class to allow optional commons-pool2 dependency.
*/
private static class PoolBuilderFactory {
LettuceClientConfiguration.LettuceClientConfigurationBuilder createBuilder(RedisProperties.Pool properties) {
return LettucePoolingClientConfiguration.builder().poolConfig(getPoolConfig(properties));
}
private GenericObjectPoolConfig<?> getPoolConfig(RedisProperties.Pool properties) {
GenericObjectPoolConfig<?> config = new GenericObjectPoolConfig<>();
config.setMaxTotal(properties.getMaxActive());
config.setMaxIdle(properties.getMaxIdle());
config.setMinIdle(properties.getMinIdle());
if (properties.getTimeBetweenEvictionRuns() != null) {
config.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRuns().toMillis());
}
if (properties.getMaxWait() != null) {
config.setMaxWaitMillis(properties.getMaxWait().toMillis());
}
return config;
}
}
}