记一次springboot关闭,redis连接池关闭堵塞问题

吴书松
吴书松
发布于 2025-05-29 / 19 阅读
0

记一次springboot关闭,redis连接池关闭堵塞问题

背景

Lettuce连接池

再common包中,增加redis的配置类,并通过META-INF.spring注入到spring容器中

原来代码

/*
 * Copyright (c) 2020 jm4cloud Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.demo.common.core.config.redis;

import com.demo.common.core.constant.CacheConstants;
import com.demo.common.core.constant.CommonConstants;
import lombok.Data;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
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.data.redis.core.*;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;

/**
 * @author jm-wss
 * @date 2019/2/1 Redis 配置类
 */
@Data
@EnableCaching
@AutoConfiguration
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class RedisTemplateConfiguration {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private Integer port;

    @Value("${spring.redis.database}")
    private Integer database;

    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.lettuce.pool.max-active:-1}")
    private Integer maxActive;
    @Value("${spring.redis.lettuce.pool.max-idle:16}")
    private Integer maxIdle;
    @Value("${spring.redis.lettuce.pool.min-idle:0}")
    private Integer minIdle;
    @Value("${spring.redis.lettuce.pool.max-wait:5000}")
    private Long maxWait;
    @Value("${spring.redis.lettuce.pool.shutdown-timeout:600}")
    private Integer shutdownTimeout;

    //秒
    @Value("${spring.redis.password.lettuce.pool.timeout:600}")
    private Integer timeout;

    @Primary
    @Bean("lettuceConnectionFactory")
    public LettuceConnectionFactory lettuceConnectionFactory(@Qualifier("redisSentinelConfiguration") RedisStandaloneConfiguration redisSentinelConfiguration
            , @Qualifier("lettuceClientConfiguration") LettuceClientConfiguration lettuceClientConfiguration) {
        return new LettuceConnectionFactory(redisSentinelConfiguration,lettuceClientConfiguration);
    }
    @Primary
    @Bean("genericObjectPoolConfig")
    public GenericObjectPoolConfig genericObjectPoolConfig() {
        GenericObjectPoolConfig<Object> poolConfig = new GenericObjectPoolConfig<>();
        poolConfig.setMaxIdle(maxIdle);
        poolConfig.setMinIdle(minIdle);
        poolConfig.setMaxTotal(maxActive);
        poolConfig.setMaxWaitMillis(maxWait);

        // 关键配置:确保连接池可以快速关闭
        poolConfig.setBlockWhenExhausted(false); // 当连接池耗尽时不阻塞
        poolConfig.setTestOnBorrow(true);        // 借出连接时测试有效性
        poolConfig.setTestOnReturn(true);        // 归还连接时测试有效性
        poolConfig.setTestWhileIdle(true);       // 空闲时测试连接
        poolConfig.setTimeBetweenEvictionRunsMillis(30000); // 30秒检查一次
        poolConfig.setMinEvictableIdleTimeMillis(20000);    // 空闲60秒可回收

        return poolConfig;
    }
    @Primary
    @Bean("lettuceClientConfiguration")
    public LettuceClientConfiguration lettuceClientConfiguration(@Qualifier("genericObjectPoolConfig") GenericObjectPoolConfig genericObjectPoolConfig) {
        return LettucePoolingClientConfiguration.builder()
                .poolConfig(genericObjectPoolConfig)
                .commandTimeout(Duration.ofSeconds(timeout))
                .shutdownTimeout(Duration.ofSeconds(shutdownTimeout))
                // 关键配置:设置优雅关闭超时
                .shutdownQuietPeriod(Duration.ofSeconds(2))
                .shutdownTimeout(Duration.ofSeconds(5))
                .build();
    }
    @Primary
    @Bean("redisSentinelConfiguration")
    public RedisStandaloneConfiguration redisSentinelConfiguration() {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setDatabase(database);
        redisStandaloneConfiguration.setHostName(host);
        redisStandaloneConfiguration.setPassword(password);
        redisStandaloneConfiguration.setPort(port);
        return redisStandaloneConfiguration;
    }

    /**
     * LettuceConnectionFactory 默认使用
     * @param factory
     * @return
     */
//  @Primary
    @Bean(name = CommonConstants.DEFAULT_REDIS)
    public RedisTemplate<String, Object> redisTemplate(@Qualifier(value = "lettuceConnectionFactory") RedisConnectionFactory factory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setHashKeySerializer(RedisSerializer.string());
        redisTemplate.setValueSerializer(RedisSerializer.java());
        redisTemplate.setHashValueSerializer(RedisSerializer.java());
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }

    @Bean(name = CacheConstants.redisTemplateJson)
    public RedisTemplate<Object, Object> redisTemplateJson(@Qualifier(value = "lettuceConnectionFactory") RedisConnectionFactory factory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setHashKeySerializer(RedisSerializer.string());

        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

        redisTemplate.setValueSerializer(serializer);
        redisTemplate.setHashValueSerializer(serializer);
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }

    @Primary
    @Bean(name = "stringRedisTemplate")
    public StringRedisTemplate stringRedisTemplate(@Qualifier(value = "lettuceConnectionFactory") RedisConnectionFactory factory) {
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
        stringRedisTemplate.setKeySerializer(RedisSerializer.string());
        stringRedisTemplate.setValueSerializer(stringSerializer());
        stringRedisTemplate.setConnectionFactory(factory);
        return stringRedisTemplate;
    }

    //自定义json序列化
    public static RedisSerializer stringSerializer() {
        return new RedisSerializer<Object>() {
            @Override
            public byte[] serialize(Object o) throws SerializationException {
                if(null == o){
                    return new byte[0];
                }
                return o.toString().getBytes(StandardCharsets.UTF_8);
            }

            @Override
            public Object deserialize(byte[] bytes) throws SerializationException {
                if (bytes == null || bytes.length <= 0) {
                    return null;
                }

                return new String(bytes, StandardCharsets.UTF_8);
            }
        };
    }

    @Bean
    public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForHash();
    }

    @Bean
    public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) {
        return redisTemplate.opsForValue();
    }

    @Bean
    public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForList();
    }

    @Bean
    public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForSet();
    }

    @Bean
    public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
        return redisTemplate.opsForZSet();
    }

    @Bean
    public CacheManager oneDayCacheManager(@Qualifier(value = "lettuceConnectionFactory") RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofDays(1))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.java()));

        return RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
    }

    @Bean(name = "flushDBScript")
    public DefaultRedisScript<Long> flushDBScript() {
        String script = "return redis.call('flushdb')";
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
        return redisScript;
    }

    @Bean(name = "checkSetHasScript")
    public DefaultRedisScript<List> checkSetHasScript() {
        String script = "local key = KEYS[1]\n" +
                "local members = ARGV\n" +
                "local exists = {}\n" +
                " \n" +
                "for i, member in ipairs(members) do\n" +
                "  exists[i] = redis.call('SISMEMBER', key, member)\n" +
                "end\n" +
                "return exists";
        DefaultRedisScript<List> redisScript = new DefaultRedisScript<>(script, List.class);
        return redisScript;
    }

    @Bean(name = "matchDelScript")
    public DefaultRedisScript<Long> matchDelScript() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(matchDelScriptText());
        redisScript.setResultType(Long.class);
        return redisScript;
    }

    private String matchDelScriptText(){
        return "local key=KEYS[1]\n" +
                "local result = 0;\n" +
                "local list=redis.call(\"keys\", key);\n" +
                "for i,v in ipairs(list) do\n" +
                "    local current =redis.call(\"del\", v);\n" +
                "    result = result + current;\n" +
                "end\n" +
                "return result;";
    }


    @Bean
    public DefaultRedisScript<Long> limitScript() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(limitScriptText());
        redisScript.setResultType(Long.class);
        return redisScript;
    }



    /**
     * 限流脚本
     */
    private String limitScriptText() {
        return "local key = KEYS[1]\n" +
                "local count = tonumber(ARGV[1])\n" +
                "local time = tonumber(ARGV[2])\n" +
                "local current = redis.call('get', key);\n" +
                "if current and tonumber(current) > count then\n" +
                "    return tonumber(current);\n" +
                "end\n" +
                "current = redis.call('incr', key)\n" +
                "if tonumber(current) == 1 then\n" +
                "    redis.call('expire', key, time)\n" +
                "end\n" +
                "return tonumber(current);";
    }

    @Bean(name = "apiLimitRedisScript")
    public DefaultRedisScript<Long> apiLimitRedisScript() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(apiLimitScriptStr());
        redisScript.setResultType(Long.class);
        return redisScript;
    }

    private static String apiLimitScriptStr() {
        return "local key = KEYS[1]\n" +
                "local a1 = ARGV[1]\n" +
                "local a2 = ARGV[2]\n" +
                "local count = tonumber(a1)\n" +
                "local time = tonumber(a2)\n" +
                "\n" +
                "if key == nil or count == nil or time == nil then\n" +
                "   error(\"param is null\")\n" +
                "end\n" +
                "\n" +
                "local current = redis.call('get', key);\n" +
                "if current and current ~= nil and tonumber(current) > count then\n" +
                "   return tonumber(current)\n" +
                "end\n" +
                "current = redis.call('incr', key);\n" +
                "if tonumber(current) == 1 then \n" +
                "   redis.call('EXPIRE',key,time)\n" +
                "end\n" +
                "return tonumber(current)";
    }

    @Bean(name = "bloomAddDataLimitScript")
    public DefaultRedisScript<Object> bloomAddDataLimitScript() {
        DefaultRedisScript<Object> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(bloomAddData());
        redisScript.setResultType(Object.class);
        return redisScript;
    }

    private static String bloomAddData() {
        return "local key = KEYS[1]\n" +
                "\n" +
                "if key == nil then\n" +
                "   error(\"param is null\")\n" +
                "end\n" +
                "local result_1 = 0\n" +
                "for k,v in ipairs(ARGV) do\n" +
                " local result_2 = redis.call('SETBIT',key,v,1)\n" +
                " result_1 = result_1 + result_2\n" +
                "end\n" +
                "return tostring(result_1)\n"
                ;
    }

    @Bean(name = "tokenBucketLimitScript")
    public DefaultRedisScript<Long> tokenBucketLimitScript() {
        return new DefaultRedisScript<>(TOKEN_BUCKET_LIMIT_SCRIPT,Long.class);
    }

    private static final String TOKEN_BUCKET_LIMIT_SCRIPT = "local tokens_key = KEYS[1]\n" +
            "local timestamp_key = KEYS[2]\n" +
            "local rate = tonumber(ARGV[1])\n" +
            "local capacity = tonumber(ARGV[2])\n" +
            "local now = tonumber(ARGV[3])\n" +
            "local requested = tonumber(ARGV[4])\n" +
            "local fill_time = capacity/rate\n" +
            "local ttl = math.floor(fill_time*2)\n" +
            "local last_tokens = tonumber(redis.call('get', tokens_key))\n" +
            "if last_tokens == nil then\n" +
            "  last_tokens = capacity\n" +
            "end\n" +
            "local last_refreshed = tonumber(redis.call('get', timestamp_key))\n" +
            "if last_refreshed == nil then\n" +
            "  last_refreshed = 0\n" +
            "end\n" +
            "local diff_time = math.max(0, now-last_refreshed)\n" +
            "local filled_tokens = math.min(capacity, last_tokens+(diff_time*rate))\n" +
            "local allowed = filled_tokens >= requested\n" +
            "local new_tokens = filled_tokens\n" +
            "local allowed_num = 0\n" +
            "if allowed then\n" +
            "  new_tokens = filled_tokens - requested\n" +
            "  allowed_num = 1\n" +
            "end\n" +
            "if ttl > 0 then\n" +
            "  redis.call('setex', tokens_key, ttl, new_tokens)\n" +
            "  redis.call('setex', timestamp_key, ttl, now)\n" +
            "end\n" +
            "return allowed_num\n";


}

kill PID 关闭springboot项目的时候,不使用-9,LettuceConnectionFactory调用.destroy();方法,进入堵塞,没办法关闭

原因:

RedisConfigurationV2是通过自己注入到spring容器中的,spring没有正常托管该类的生命周期状态,需要自己释放对象

package com.demo.common.core.config.redis;

import com.google.common.base.Charsets;
import com.google.common.hash.Funnel;
import com.demo.common.core.config.bloom.BloomFilterHelper;
import com.demo.common.core.config.bloom.RedisBloomFilter;
import com.demo.common.core.config.bloom.RedisBloomUtils;
import com.demo.common.core.constant.CacheConstants;
import io.lettuce.core.resource.ClientResources;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
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.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

import javax.annotation.PreDestroy;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

@Data
@Slf4j
@AutoConfiguration
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class RedisConfigurationV2 implements DisposableBean {

    @Value("${redis2.host}")
    private String host2;

    @Value("${redis2.port}")
    private Integer port2;

    @Value("${redis2.database}")
    private Integer database2;

    @Value("${redis2.password}")
    private String password2;

    @Value("${redis2.client.threadPoolSize:4}")
    private Integer clientThreadPoolSize;

    @Value("${redis2.client.cThreadPoolSize:4}")
    private Integer clientCThreadPoolSize;

    @Value("${redis2.lettuce.pool.max-active:-1}")
    private Integer maxActive;
    @Value("${redis2.lettuce.pool.max-idle:16}")
    private Integer maxIdle;
    @Value("${redis2.lettuce.pool.min-idle:0}")
    private Integer minIdle;
    @Value("${redis2.lettuce.pool.max-wait:5000}")
    private Long maxWait;
    //秒
    @Value("${redis2.lettuce.shutdown-timeout:600}")
    private Integer shutdownTimeout;
    //秒
    @Value("${redis2.lettuce.timeout:600}")
    private Integer timeout;

    private LettuceConnectionFactory lettuceConnectionFactory;
    private ClientResources clientResources; // 显式管理ClientResources
    private final AtomicBoolean destroyed = new AtomicBoolean(false);

    @Bean("lettuceConnectionFactoryV2")
    public LettuceConnectionFactory lettuceConnectionFactoryV2(
            @Qualifier("redisSentinelConfigurationV2") RedisStandaloneConfiguration redisConfig,
            @Qualifier("lettuceClientConfigurationV2") LettuceClientConfiguration lettuceConfig) {
        if ("-1".equals(host2)) {
            return null;
        }
        this.lettuceConnectionFactory = new LettuceConnectionFactory(redisConfig, lettuceConfig);
        return this.lettuceConnectionFactory;
    }

    // 添加销毁方法
    @PreDestroy
    public void preDestroy() {
        log.info("preDestroy");
        shutdownRedisResources();
    }

    // 双保险机制:实现DisposableBean接口
    @Override
    public void destroy() throws Exception {
        log.info("destroy");
        shutdownRedisResources();
    }

    private static RedisConfigurationV2 instance;
    public RedisConfigurationV2() {
        instance = this; // 保存当前实例的引用
    }
    // 双保险机制:静态关闭钩子
    static {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            if (instance != null) {
                instance.shutdownRedisResources();
            }
        }));
    }

    /**
     * 安全关闭Redis资源(带超时和强制终止)
     */
    /**
     * 安全关闭Redis资源(带超时和强制终止)
     */
    private synchronized void shutdownRedisResources() {
        if (destroyed.get()) return;
        destroyed.set(true);

        if (lettuceConnectionFactory == null) return;

        log.info("Attempting to close Redis2 connection resources...");

        // 1. 尝试关闭连接工厂
        tryGracefulShutdown();

        // 2. 强制关闭连接池
        forceCloseConnectionPool();

        // 3. 清理Lettuce资源(使用显式管理的ClientResources)
        cleanupLettuceResources();

        log.info("Redis2 resources released");
    }


    /**
     * 尝试优雅关闭连接
     */
    private void tryGracefulShutdown() {
        try {
            ExecutorService executor = Executors.newSingleThreadExecutor();
            executor.submit(() -> {
                try {
                    lettuceConnectionFactory.destroy();
                    log.info("Redis2 connections closed gracefully");
                } catch (Exception e) {
                    log.warn("Graceful shutdown failed: {}", e.getMessage());
                }
            }).get(15, TimeUnit.SECONDS); // 设置15秒超时
            executor.shutdownNow();
        } catch (Exception e) {
            log.warn("Graceful shutdown timed out: {}", e.getMessage());
        }
    }

    /**
     * 强制关闭连接池
     */
    private void forceCloseConnectionPool() {
        try {
            // 直接关闭连接池(不依赖反射)
            if (lettuceConnectionFactory != null) {
                // 调用resetConnection()释放所有连接
                lettuceConnectionFactory.resetConnection();

                // 关闭连接池(如果可用)
//              if (lettuceConnectionFactory.getPool() != null) {
//                  lettuceConnectionFactory.getPool().close();
//                  log.info("Forcibly closed connection pool");
//              }
            }
        } catch (Exception e) {
            log.warn("Force closure of connection pool failed: {}", e.getMessage());
        }
    }

    /**
     * 清理Lettuce底层资源
     */
    private void cleanupLettuceResources() {
        if (clientResources != null) {
            try {
                clientResources.shutdown().get(5, TimeUnit.SECONDS);
                log.info("Lettuce client resources shutdown");
            } catch (Exception e) {
                log.warn("Failed to shutdown Lettuce client resources: {}", e.getMessage());
            }
        }
    }

    // 显式创建并管理ClientResources
    @Bean("clientResourcesV2")
    public ClientResources clientResourcesV2() {
        this.clientResources = ClientResources.builder()
                .ioThreadPoolSize(clientThreadPoolSize)  // 根据需求调整
                .computationThreadPoolSize(clientCThreadPoolSize) // 根据需求调整
                .build();
        return this.clientResources;
    }


    @Bean("genericObjectPoolConfigV2")
    public GenericObjectPoolConfig genericObjectPoolConfigV2() {
        GenericObjectPoolConfig<Object> poolConfig = new GenericObjectPoolConfig<>();
        poolConfig.setMaxIdle(maxIdle);
        poolConfig.setMinIdle(minIdle);
        poolConfig.setMaxTotal(maxActive);
        poolConfig.setMaxWaitMillis(maxWait);

        // 关键配置:确保连接池可以快速关闭
        poolConfig.setBlockWhenExhausted(false); // 当连接池耗尽时不阻塞
        poolConfig.setTestOnBorrow(true);        // 借出连接时测试有效性
        poolConfig.setTestOnReturn(true);        // 归还连接时测试有效性
        poolConfig.setTestWhileIdle(true);       // 空闲时测试连接
        poolConfig.setTimeBetweenEvictionRunsMillis(30000); // 30秒检查一次
        poolConfig.setMinEvictableIdleTimeMillis(20000);    // 空闲60秒可回收

        return poolConfig;
    }

    @Bean("lettuceClientConfigurationV2")
    public LettuceClientConfiguration lettuceClientConfigurationV2(@Qualifier("genericObjectPoolConfigV2") GenericObjectPoolConfig genericObjectPoolConfig,
                                                                   @Qualifier("clientResourcesV2") ClientResources clientResources) {
        return LettucePoolingClientConfiguration.builder()
                .poolConfig(genericObjectPoolConfig)
                .commandTimeout(Duration.ofSeconds(timeout))
                .shutdownTimeout(Duration.ofSeconds(shutdownTimeout))
                // 关键配置:设置优雅关闭超时
                .shutdownQuietPeriod(Duration.ofSeconds(2))
                .shutdownTimeout(Duration.ofSeconds(5))
                .clientResources(clientResources) // 使用显式管理的ClientResources
                .build();
    }

    @Bean("redisSentinelConfigurationV2")
    public RedisStandaloneConfiguration redisSentinelConfigurationV2() {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setDatabase(database2);
        redisStandaloneConfiguration.setHostName(host2);
        redisStandaloneConfiguration.setPassword(password2);
        redisStandaloneConfiguration.setPort(port2);
        return redisStandaloneConfiguration;
    }

    /**
     * 注意Bean名称不能重复
     * @return redisBean
     */
    @Bean(name = CacheConstants.BLOOM_REDIS_TEMPLATE)
    public RedisTemplate<String, Object> redisIotTemplate(@Qualifier("lettuceConnectionFactoryV2") LettuceConnectionFactory lettuceConnectionFactory) {
        if("-1".equals(host2)){
            return null;
        }
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setHashKeySerializer(RedisSerializer.string());

        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

        redisTemplate.setValueSerializer(serializer);
//      redisTemplate.setValueSerializer(RedisSerializer.java());
        redisTemplate.setHashValueSerializer(serializer);
//      redisTemplate.setHashValueSerializer(RedisSerializer.java());
        redisTemplate.setConnectionFactory(
                lettuceConnectionFactory
        );
        log.info("多版本redis构建成功!!!");
        return redisTemplate;
    }


    @Bean(name = CacheConstants.BLOOM_REDIS_TEMPLATE_STRING)
    public StringRedisTemplate stringRedisTemplateIot(@Qualifier("lettuceConnectionFactoryV2") LettuceConnectionFactory factory) {
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
        stringRedisTemplate.setKeySerializer(RedisSerializer.string());
        stringRedisTemplate.setValueSerializer(stringSerializer());
        stringRedisTemplate.setConnectionFactory(factory);
        return stringRedisTemplate;
    }

    public static RedisSerializer stringSerializer() {
        return new RedisSerializer<Object>() {
            @Override
            public byte[] serialize(Object o) throws SerializationException {
                if(null == o){
                    return new byte[0];
                }
                return o.toString().getBytes(StandardCharsets.UTF_8);
            }

            @Override
            public Object deserialize(byte[] bytes) throws SerializationException {
                if (bytes == null || bytes.length == 0) {
                    return null;
                }

                return new String(bytes, StandardCharsets.UTF_8);
            }
        };
    }


    /**
     * 布隆过滤器预计数据量:50000000
     */
    private static final Integer expectedInsertions = 50000000;
    /**
     * 布隆过滤器精度
     */
    private static final Double fpp = 0.00001;


    /**
     * 初始化布隆过滤器,放入到spring容器里面
     *
     * @return BloomFilterHelper1
     */
    @Bean(value = "bloomFilterHelper")
    public BloomFilterHelper<String> bloomFilterHelper() {
        return new BloomFilterHelper<>((Funnel<String>) (from, into) -> into.putString(from, Charsets.UTF_8).putString(from, Charsets.UTF_8), expectedInsertions, fpp);
    }

    @Bean(value = "redisBloomFilter")
    public RedisBloomFilter redisBloomFilter(@Qualifier(value = "bloomFilterHelper") BloomFilterHelper bloomFilterHelper,
                                             @Qualifier(value = "bloomAddDataLimitScript") RedisScript<Object> bloomLimitScript,
                                             @Qualifier(value = CacheConstants.BLOOM_REDIS_TEMPLATE_STRING) StringRedisTemplate stringRedisTemplateIot){
        return new RedisBloomFilter(bloomFilterHelper,bloomLimitScript,stringRedisTemplateIot);
    }

    @Bean(value = "redisBloomUtils")
    public RedisBloomUtils redisBloomUtils(  @Qualifier(value = CacheConstants.BLOOM_REDIS_TEMPLATE_STRING) StringRedisTemplate stringRedisTemplateIot){
        return new RedisBloomUtils(stringRedisTemplateIot);
    }


}