目的:利用Spring-data-redis,创建lettuce原生的客户端,并兼容单节点哨兵以及集群三种模式

欢迎指正

package com.demo.core.config;

import com.alibaba.fastjson.JSONObject;
import io.lettuce.core.AbstractRedisClient;
import io.lettuce.core.ClientOptions;
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import io.lettuce.core.cluster.RedisClusterClient;
import io.lettuce.core.protocol.DecodeBufferPolicies;
import io.lettuce.core.resource.ClientResources;
import io.lettuce.core.support.ConnectionPoolSupport;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;

/**
 * @author: luochenzc
 * @create: 2023/3/30
 * @Description:
 * @FileName: RedisConfig
 */
@Configuration
@Slf4j
public class RedisConfig {

    @Resource
    RedisProperties redisProperties;

    @Bean
    @ConfigurationProperties(prefix = "spring.redis.lettuce.pool")
    public GenericObjectPoolConfig genericObjectPoolConfig() {
        return new GenericObjectPoolConfig();
    }

    @Bean
    ClusterTopologyRefreshOptions clusterTopologyRefreshOptions() {
        ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
                .adaptiveRefreshTriggersTimeout(Duration.ofSeconds(5L))//设置自适应拓扑刷新超时,每次超时刷新一次,默认30s;
                .closeStaleConnections(false)//刷新拓扑时是否关闭失效连接,默认true,isPeriodicRefreshEnabled()为true时生效;
                .dynamicRefreshSources(true)//从拓扑中发现新节点,并将新节点也作为拓扑的源节点,动态刷新可以发现全部节点并计算每个客户端的数量,设置false则只有初始节点为源和计算客户端数量;
                .enableAllAdaptiveRefreshTriggers()//启用全部触发器自适应刷新拓扑,默认关闭;
                .enablePeriodicRefresh(Duration.ofSeconds(5L))//开启定时拓扑刷新并设置周期;
                .refreshTriggersReconnectAttempts(3)//长连接重新连接尝试n次才拓扑刷新
                .build();
        return clusterTopologyRefreshOptions;
    }

    //配置集群选项,自动重连,最多重定型1次
    @Bean
    ClientOptions clusterClientOptions(ClusterTopologyRefreshOptions clusterTopologyRefreshOptions) {
        ClientOptions clientOptions;
        if (redisProperties.getCluster() != null) {
            clientOptions = ClusterClientOptions.builder()
                    .autoReconnect(true)//在连接丢失时开启或关闭自动重连,默认true;
                    .decodeBufferPolicy(DecodeBufferPolicies.always())//设置丢弃解码缓冲区的策略,以回收内存;always:解码后丢弃,最大内存效率;alwaysSome:解码后丢弃一部分;ratio(n)基于比率丢弃,n/(1+n),通常用1-10对应50%-90%;
                    .disconnectedBehavior(ClientOptions.DisconnectedBehavior.DEFAULT)//设置连接断开时命令的调用行为,默认启用重连;DEFAULT:启用时重连中接收命令,禁用时重连中拒绝命令;ACCEPT_COMMANDS:重连中接收命令;REJECT_COMMANDS:重连中拒绝命令;
                    .maxRedirects(redisProperties.getCluster().getMaxRedirects())//当键从一个节点迁移到另一个节点,集群重定向次数,默认5;
//                  .cancelCommandsOnReconnectFailure(true)//允许在重连失败取消排队命令,默认false;
//				    .nodeFilter(nodeFilter)//设置节点过滤器
//				    .pingBeforeActivateConnection(true)//激活连接前设置PING,默认true;
//				    .protocolVersion(ProtocolVersion.RESP3)//设置协议版本,默认RESP3;
//				    .publishOnScheduler(false)//使用专用的调度器发出响应信号,默认false,启用时数据信号将使用服务的多线程发出;
//				    .requestQueueSize(requestQueueSize)//设置每个连接请求队列大小;
//				    .scriptCharset(scriptCharset)//设置Lua脚本编码为byte[]的字符集,默认StandardCharsets.UTF_8;
//				    .socketOptions(SocketOptions.builder().connectTimeout(Duration.ofSeconds(10)).keepAlive(true).tcpNoDelay(true).build())//设置低级套接字的属性
//				    .sslOptions(SslOptions.builder().build())//设置ssl属性
//				    .suspendReconnectOnProtocolFailure(false)//当重新连接遇到协议失败时暂停重新连接(SSL验证,连接失败前PING),默认值为false;
//				    .timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(10)))//设置超时来取消和终止命令;
//                  .validateClusterNodeMembership(true)//在允许连接到集群节点之前,验证集群节点成员关系,默认值为true;
                    .topologyRefreshOptions(clusterTopologyRefreshOptions)//设置拓扑更新设置
                    .build();
        } else {
            clientOptions = ClientOptions.builder().autoReconnect(true).build();
        }
        return clientOptions;
    }


    //创建集群客户端
    @Bean
    AbstractRedisClient abstractRedisClient(ClientResources clientResources, ClientOptions clientOptions) {
        if (redisProperties.getCluster() != null) {
            List<RedisURI> servers = new ArrayList<>();
            List<String> nodes = redisProperties.getCluster().getNodes();
            if (CollectionUtils.isEmpty(nodes)) {
                log.info("redis config error! redisProperties:{}", JSONObject.toJSONString(redisProperties));
                System.exit(1);
            }
            for (String node : nodes) {
                String[] nodeAddress = node.split(":");
                if (nodeAddress == null || nodeAddress.length != 2) {
                    log.info("redis config error! redisProperties:{}", JSONObject.toJSONString(redisProperties));
                    System.exit(1);
                }
                servers.add(RedisURI.builder().withHost(nodeAddress[0]).withPort(Integer.valueOf(nodeAddress[1]))
                        .withAuthentication(redisProperties.getUsername(), redisProperties.getPassword())
                        .withDatabase(redisProperties.getDatabase()).build());
            }
            RedisClusterClient redisClusterClient = RedisClusterClient.create(clientResources, servers);
            ClusterClientOptions clusterClientOptions = (ClusterClientOptions) clientOptions;
            redisClusterClient.setOptions(clusterClientOptions);
            return redisClusterClient;
        } else if (redisProperties.getSentinel() != null) {
            List<String> nodes = redisProperties.getSentinel().getNodes();
            if (CollectionUtils.isEmpty(nodes)) {
                log.info("redis config error! redisProperties:{}", JSONObject.toJSONString(redisProperties));
                System.exit(1);
            }
            RedisURI.Builder builder = RedisURI.builder();
            for (String node : nodes) {
                String[] nodeAddress = node.split(":");
                if (nodeAddress == null || nodeAddress.length != 2) {
                    log.info("redis config error! redisProperties:{}", JSONObject.toJSONString(redisProperties));
                    System.exit(1);
                }
                builder.withSentinel(nodeAddress[0], Integer.valueOf(nodeAddress[1]), redisProperties.getPassword());
            }
            String username = redisProperties.getUsername();
            if (StringUtils.isBlank(username)) {
                username = "default";
            }
            RedisURI redisURI = builder.withAuthentication(username,
                            redisProperties.getPassword()).withDatabase(redisProperties.getDatabase())
                    .withSentinelMasterId(redisProperties.getSentinel().getMaster()).build();
            RedisClient redisClient = RedisClient.create(redisURI);
            redisClient.setOptions(clientOptions);
            return redisClient;
        } else {
            String username = redisProperties.getUsername();
            if (StringUtils.isBlank(username)) {
                username = "default";
            }
            RedisURI.Builder builder = RedisURI.builder();
            RedisURI redisURI = builder
                    .withHost(redisProperties.getHost())
                    .withPort(redisProperties.getPort())
                    .withAuthentication(username, redisProperties.getPassword())
                    .withDatabase(redisProperties.getDatabase()).build();
            RedisClient redisClient = RedisClient.create(redisURI);
            redisClient.setOptions(clientOptions);
            return redisClient;
        }
    }


    /**
     * 集群连接
     */
    @Bean(destroyMethod = "close")
    StatefulRedisConnection<String, String> statefulRedisConnection(GenericObjectPoolConfig genericObjectPoolConfig, 
                                                                    AbstractRedisClient abstractRedisClient) throws Exception {
        GenericObjectPool<StatefulRedisConnection<String, String>> pool;
        if (redisProperties.getCluster() != null) {
            RedisClusterClient redisClusterClient = (RedisClusterClient) abstractRedisClient;
            pool = ConnectionPoolSupport.createGenericObjectPool(redisClusterClient::connectPubSub, genericObjectPoolConfig);
        } else {
            RedisClient redisClient = (RedisClient) abstractRedisClient;
            pool = ConnectionPoolSupport.createGenericObjectPool(redisClient::connect, genericObjectPoolConfig);
        }
        return pool.borrowObject();
    }

}