0


SpringBoot Redis 多数据源集成支持哨兵模式和Cluster集群模式

Redis 从入门到精通【应用篇】之SpringBoot Redis 多数据源集成支持哨兵模式Cluster集群模式、单机模式

文章目录

在这里插入图片描述

0.前言

说明

大家都知道Redis在6.0版本之前是单线程工作的,这导致在一个项目中有大量读写操作的情况下,Redis单实例的性能被其他业务长时间占据,导致部分业务出现延迟现象,为了解决这个问题,部分公司项目选择使用多个Redis实例分别存储不同的业务数据和使用场景,比如IoT网关写入的数据,可以单独拆分一个Redis实例去使用,其他业务使用一个Redis实例。用多个Redis实例 可以提高Redis的性能。Redis是一种基于内存的缓存数据库,内存容量是其性能的瓶颈。当项目中的数据量较大时,单个Redis实例可能无法承载所有数据,导致性能下降。而使用多个Redis实例可以将数据分散到多个实例中,从而提高Redis的整体性能。

这就导致在某些业务场景下,一个项目工程,同时要使用这两个Redis实例的数据,这就是本文要解决的问题。

本文通过写一个Redis 多数据源组件 Starter 来解决上面的问题,支持Redis 多数据源,可集成配置哨兵模式、Cluster集群模式、单机模式。如果单实例配置哨兵模式,请参阅我之前的博客 《SpringBoot Redis 使用Lettuce和Jedis配置哨兵模式》

项目结构

在这里插入图片描述

Pom 依赖

如下,可能有多余的,根据项目具体情况删减。再就是需要使用

Springboot parent 
<spring-boot-dependencies.version>2.7.12</spring-boot-dependencies.version>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-json</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>transmittable-thread-local</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency>

1. 配置

1.1 通用配置()

设置主redis的标识

很关键

# 示例custom.primary.redis.key=user

连接池相关配置

此配置为通用配置所有类型的链接模式都可以配置,不配置走Springboot 默认配置。

spring.redis.xxx.timeout =3000
spring.redis.xxx.maxTotal =50
spring.redis.xxx.maxIdle =50
spring.redis.xxx.minIdle =2
spring.redis.xxx.maxWaitMillis =10000
spring.redis.xxx.testOnBorrow = False

1.2 单例模式配置

# 第1个Redis 实例 用于用户体系,我们取标识为user
spring.redis.user.host =127.0.0.1
spring.redis.user.port =6380
spring.redis.user.password = 密码
spring.redis.user.database =0# 第2个Redis 实例用于IoT体系
spring.redis.iot.host =127.0.0.1
spring.redis.iot.port =6390
spring.redis.iot.password = 密码
spring.redis.iot.database =0# 第3个Redis 实例用于xxx
spring.redis.xxx.host =127.0.0.1
spring.redis.xxx.port =6390
spring.redis.xxx.password = 密码
spring.redis.xxx.database =0

1.3 哨兵模式配置

多个Redis数据库实例的情况下,将下面配置项多配置几个。

spring.redis.xxx1.sentinel.master=mymaster1
spring.redis.xxx1.sentinel.nodes=ip:端口,ip:端口
spring.redis.xxx1.password = bD945aAfeb422E22AbAdFb9D2a22bEDd
spring.redis.xxx1.database =0
spring.redis.xxx1.timeout =3000#第二个spring.redis.xxx2.sentinel.master=mymaster2
spring.redis.xxx2.sentinel.nodes=ip:端口,ip:端口
spring.redis.xxx2.password = bD945aAfeb422E22AbAdFb9D2a22bEDd
spring.redis.xxx2.database =0
spring.redis.xxx2.timeout =3000

1.4 集群模式配置(集群模式不支持设置database)

spring.redis.xxx1.cluster.nodes=ip1:端口,ip2:端口,ip3:端口,ip4:端口,ip5:端口,ip6:端口
spring.redis.xxx1.cluster.max-redirects=5
spring.redis.xxx1.password = 密码
spring.redis.xxx1.timeout =3000

2. 代码实例

2.1.CustomRedisConfig

根据配置文件配置项,创建Redis多个数据源的

RedisTemplate


主要思想为,

  1. 在服务启动过程中读取多数据源配置文件,将多数据源的配置读取到
// 定义静态Map变量redis,用于存储Redis配置参数protectedstaticMap<String,Map<String,String>> redis =newHashMap<>();
  1. 根据多数据源配置创建不同类型的Configuration
privateRedisStandaloneConfigurationbuildStandaloneConfig(Map<String,String> param){//...省略}
privateRedisSentinelConfigurationbuildSentinelConfig(Map<String,String> param){//...省略}
privateRedisClusterConfigurationbuildClusterConfig(Map<String,String> param){//...省略}
  1. 根据 不同类型的创建RedisConnectionFactory
publicRedisConnectionFactorybuildLettuceConnectionFactory(String redisKey,Map<String,String> param,GenericObjectPoolConfig genericObjectPoolConfig){...}

4.最后遍历上面我们配置的配置文件调用

buildCustomRedisService(k, redisTemplate, stringRedisTemplate);

将创建的不同的RedisTemplate Bean 然后注入到Spring容器中

CustomRedisConfig 源码

源码中涉及的Springboot 相关知识在此处就不做赘婿,需要了解,可以参考我的《SpringBoot 源码解析系列》

InitializingBean,
ApplicationContextAware

,

BeanPostProcessor
packagecom.iceicepip.project.common.redis;importcom.iceicepip.project.common.redis.util.AddressUtils;importcom.fasterxml.jackson.annotation.JsonAutoDetect;importcom.fasterxml.jackson.annotation.PropertyAccessor;importcom.fasterxml.jackson.databind.ObjectMapper;importorg.apache.commons.lang3.StringUtils;importorg.apache.commons.pool2.impl.GenericObjectPoolConfig;importorg.springframework.beans.MutablePropertyValues;importorg.springframework.beans.factory.InitializingBean;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.beans.factory.config.BeanPostProcessor;importorg.springframework.beans.factory.config.ConstructorArgumentValues;importorg.springframework.beans.factory.support.DefaultListableBeanFactory;importorg.springframework.beans.factory.support.GenericBeanDefinition;importorg.springframework.boot.autoconfigure.AutoConfiguration;importorg.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;importorg.springframework.boot.context.properties.ConfigurationProperties;importorg.springframework.context.ApplicationContext;importorg.springframework.context.ApplicationContextAware;importorg.springframework.context.annotation.Bean;importorg.springframework.core.env.MapPropertySource;importorg.springframework.core.env.StandardEnvironment;importorg.springframework.data.redis.connection.*;importorg.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;importorg.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;importorg.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;importorg.springframework.data.redis.serializer.StringRedisSerializer;importjava.time.Duration;importjava.util.*;@AutoConfiguration@ConfigurationProperties(prefix ="spring")publicclassCustomRedisConfigimplementsInitializingBean,ApplicationContextAware,BeanPostProcessor{// 定义静态Map变量redis,用于存储Redis配置参数protectedstaticMap<String,Map<String,String>> redis =newHashMap<>();// 在代码中作为Redis的主数据源的标识@Value("${customer.primary.redis.key}")privateString primaryKey;@Override// 实现InitializingBean接口的方法,用于在属性被注入后初始化Redis连接工厂和Redis模板publicvoidafterPropertiesSet(){
        redis.forEach((k, v)->{// 如果当前的Redis主键等于注入的主键,则将Redis配置参数加入到属性源中if(Objects.equals(k,primaryKey)){Map<String,Object> paramMap =newHashMap<>(4);
                v.forEach((k1,v1)-> paramMap.put("spring.redis."+k1,v1));MapPropertySource mapPropertySource =newMapPropertySource("redisAutoConfigProperty", paramMap);((StandardEnvironment)applicationContext.getEnvironment()).getPropertySources().addLast(mapPropertySource);}// 创建Redis连接池配置对象和连接工厂对象GenericObjectPoolConfig genericObjectPoolConfig =buildGenericObjectPoolConfig(k, v);RedisConnectionFactory lettuceConnectionFactory =buildLettuceConnectionFactory(k, v, genericObjectPoolConfig);// 创建Redis模板对象和字符串模板对象,并调用方法创建自定义Redis服务对象RedisTemplate redisTemplate =buildRedisTemplate(k, lettuceConnectionFactory);StringRedisTemplate stringRedisTemplate =buildStringRedisTemplate(k, lettuceConnectionFactory);buildCustomRedisService(k, redisTemplate, stringRedisTemplate);});}// 创建Redis主数据源 RedisTemplate@BeanpublicRedisTemplate<Object,Object>redisTemplate(){Map<String,String> redisParam = redis.get(primaryKey);GenericObjectPoolConfig genericObjectPoolConfig =buildGenericObjectPoolConfig(primaryKey, redisParam);RedisConnectionFactory lettuceConnectionFactory =buildLettuceConnectionFactory(primaryKey, redisParam, genericObjectPoolConfig);RedisTemplate<Object,Object> template =newRedisTemplate();
        template.setConnectionFactory(lettuceConnectionFactory);return template;}//  创建Redis主数据源 StringRedisTemplate @Bean@ConditionalOnMissingBeanpublicStringRedisTemplatestringRedisTemplate(){Map<String,String> redisParam = redis.get(primaryKey);GenericObjectPoolConfig genericObjectPoolConfig =buildGenericObjectPoolConfig(primaryKey, redisParam);RedisConnectionFactory lettuceConnectionFactory =buildLettuceConnectionFactory(primaryKey, redisParam, genericObjectPoolConfig);StringRedisTemplate template =newStringRedisTemplate();
        template.setConnectionFactory(lettuceConnectionFactory);return template;}// 创建自定义Redis服务对象privatevoidbuildCustomRedisService(String k,RedisTemplate redisTemplate,StringRedisTemplate stringRedisTemplate){ConstructorArgumentValues constructorArgumentValues =newConstructorArgumentValues();
        constructorArgumentValues.addIndexedArgumentValue(0, stringRedisTemplate);
        constructorArgumentValues.addIndexedArgumentValue(1, redisTemplate);// 将来使用的时候Redis对象的beanName,区分多个数据源setCosBean(k +"Redis",CustomRedisService.class, constructorArgumentValues);}// 创建StringRedisTemplateprivateStringRedisTemplatebuildStringRedisTemplate(String k,RedisConnectionFactory lettuceConnectionFactory){ConstructorArgumentValues constructorArgumentValues =newConstructorArgumentValues();
        constructorArgumentValues.addIndexedArgumentValue(0, lettuceConnectionFactory);setCosBean(k +"StringRedisTemplate",StringRedisTemplate.class, constructorArgumentValues);returngetBean(k +"StringRedisTemplate");}// 创建Redis模板对象privateRedisTemplatebuildRedisTemplate(String k,RedisConnectionFactory lettuceConnectionFactory){// 如果已经存在Redis模板对象,则直接返回该对象if(applicationContext.containsBean(k +"RedisTemplate")){returngetBean(k +"RedisTemplate");}// 创建Redis序列化器对象Jackson2JsonRedisSerializer serializer =newJackson2JsonRedisSerializer(Object.class);ObjectMapper mapper =newObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);// 创建Redis模板对象,并设置连接池配置和序列化器等属性Map original =newHashMap<>(2);
        original.put("connectionFactory", lettuceConnectionFactory);
        original.put("valueSerializer", serializer);
        original.put("keySerializer",newStringRedisSerializer());
        original.put("hashKeySerializer",newStringRedisSerializer());
        original.put("hashValueSerializer", serializer);// 将来使用RedisTemplate的地方只需要用注解制定beanName 即可获取到每个Redis实例的操作工具类setBean(k +"RedisTemplate",RedisTemplate.class, original);returngetBean(k +"RedisTemplate");}}publicGenericObjectPoolConfigbuildGenericObjectPoolConfig(String redisKey,Map<String,String> param){if(applicationContext.containsBean(redisKey +"GenericObjectPoolConfig")){returngetBean(redisKey +"GenericObjectPoolConfig");}Integer maxIdle =StringUtils.isEmpty(param.get(CustomRedisConfigConstant.REDIS_MAXIDLE))?GenericObjectPoolConfig.DEFAULT_MAX_IDLE:Integer.valueOf(param.get(CustomRedisConfigConstant.REDIS_MAXIDLE));Integer minIdle =StringUtils.isEmpty(param.get(CustomRedisConfigConstant.REDIS_MINIDLE))?GenericObjectPoolConfig.DEFAULT_MIN_IDLE:Integer.valueOf(param.get(CustomRedisConfigConstant.REDIS_MINIDLE));Integer maxTotal =StringUtils.isEmpty(param.get(CustomRedisConfigConstant.REDIS_MAXTOTAL))?GenericObjectPoolConfig.DEFAULT_MAX_TOTAL:Integer.valueOf(param.get(CustomRedisConfigConstant.REDIS_MAXTOTAL));Long maxWaitMillis =StringUtils.isEmpty(param.get(CustomRedisConfigConstant.REDIS_MAXWAITMILLIS))?-1L:Long.valueOf(param.get(CustomRedisConfigConstant.REDIS_MAXWAITMILLIS));Boolean testOnBorrow =StringUtils.isEmpty(param.get(CustomRedisConfigConstant.REDIS_TESTONBORROW))?Boolean.FALSE:Boolean.valueOf(param.get(CustomRedisConfigConstant.REDIS_TESTONBORROW));Map original =newHashMap<>(8);
        original.put("maxTotal", maxTotal);
        original.put("maxIdle", maxIdle);
        original.put("minIdle", minIdle);
        original.put("maxWaitMillis",maxWaitMillis);
        original.put("testOnBorrow",testOnBorrow);setBean(redisKey +"GenericObjectPoolConfig",GenericObjectPoolConfig.class, original);returngetBean(redisKey +"GenericObjectPoolConfig");}publicRedisConnectionFactorybuildLettuceConnectionFactory(String redisKey,Map<String,String> param,GenericObjectPoolConfig genericObjectPoolConfig){if(applicationContext.containsBean(redisKey +"redisConnectionFactory")){returngetBean(redisKey +"redisConnectionFactory");}String timeout =StringUtils.defaultIfEmpty(param.get(CustomRedisConfigConstant.REDIS_TIMEOUT),"3000");LettuceClientConfiguration clientConfig =LettucePoolingClientConfiguration.builder().commandTimeout(Duration.ofMillis(Long.valueOf(timeout))).poolConfig(genericObjectPoolConfig).build();Object firstArgument =null;if(this.isCluster(param)){RedisClusterConfiguration clusterConfiguration =buildClusterConfig(param);
            firstArgument = clusterConfiguration;}elseif(this.isSentinel(param)){RedisSentinelConfiguration sentinelConfiguration =buildSentinelConfig(param);
            firstArgument = sentinelConfiguration;}else{RedisStandaloneConfiguration standaloneConfiguration =buildStandaloneConfig(param);
            firstArgument = standaloneConfiguration;}ConstructorArgumentValues constructorArgumentValues =newConstructorArgumentValues();
        constructorArgumentValues.addIndexedArgumentValue(0, firstArgument);
        constructorArgumentValues.addIndexedArgumentValue(1, clientConfig);setCosBean(redisKey +"redisConnectionFactory",LettuceConnectionFactory.class, constructorArgumentValues);returngetBean(redisKey +"redisConnectionFactory");}/**
     * 如果配置的是哨兵模式
     * @return
     */privatebooleanisSentinel(Map<String,String> param){String sentinelMaster = param.get(CustomRedisConfigConstant.REDIS_SENTINEL_MASTER);String sentinelNodes = param.get(CustomRedisConfigConstant.REDIS_SENTINEL_NODES);returnStringUtils.isNotEmpty(sentinelMaster)&&StringUtils.isNotEmpty(sentinelNodes);}/**
     * 如果配置的是集群模式
     * @return
     */privatebooleanisCluster(Map<String,String> param){String clusterNodes = param.get(CustomRedisConfigConstant.REDIS_CLUSTER_NODES);returnStringUtils.isNotEmpty(clusterNodes);}privateRedisStandaloneConfigurationbuildStandaloneConfig(Map<String,String> param){String host = param.get(CustomRedisConfigConstant.REDIS_HOST);String port = param.get(CustomRedisConfigConstant.REDIS_PORT);String database = param.get(CustomRedisConfigConstant.REDIS_DATABASE);String password = param.get(CustomRedisConfigConstant.REDIS_PASSWORD);RedisStandaloneConfiguration standaloneConfig =newRedisStandaloneConfiguration();
        standaloneConfig.setHostName(host);
        standaloneConfig.setDatabase(Integer.valueOf(database));
        standaloneConfig.setPort(Integer.valueOf(port));
        standaloneConfig.setPassword(RedisPassword.of(password));return standaloneConfig;}privateRedisSentinelConfigurationbuildSentinelConfig(Map<String,String> param){String sentinelMaster = param.get(CustomRedisConfigConstant.REDIS_SENTINEL_MASTER);String sentinelNodes = param.get(CustomRedisConfigConstant.REDIS_SENTINEL_NODES);String database = param.get(CustomRedisConfigConstant.REDIS_DATABASE);String password = param.get(CustomRedisConfigConstant.REDIS_PASSWORD);RedisSentinelConfiguration config =newRedisSentinelConfiguration();
        config.setMaster(sentinelMaster);Iterable<AddressUtils.Address> addressIterable =AddressUtils.parseAddresses(sentinelNodes);Iterable<RedisNode> redisNodes =transform(addressIterable);

        config.setDatabase(Integer.valueOf(database));
        config.setPassword(RedisPassword.of(password));
        config.setSentinels(redisNodes);return config;}privateRedisClusterConfigurationbuildClusterConfig(Map<String,String> param){String clusterNodes = param.get(CustomRedisConfigConstant.REDIS_CLUSTER_NODES);String clusterMaxRedirects = param.get(CustomRedisConfigConstant.REDIS_CLUSTER_MAX_REDIRECTS);String password = param.get(CustomRedisConfigConstant.REDIS_PASSWORD);RedisClusterConfiguration config =newRedisClusterConfiguration();Iterable<AddressUtils.Address> addressIterable =AddressUtils.parseAddresses(clusterNodes);Iterable<RedisNode> redisNodes =transform(addressIterable);
        config.setClusterNodes(redisNodes);
        config.setMaxRedirects(StringUtils.isEmpty(clusterMaxRedirects)?5:Integer.valueOf(clusterMaxRedirects));
        config.setPassword(RedisPassword.of(password));return config;}privateIterable<RedisNode>transform(Iterable<AddressUtils.Address> addresses){List<RedisNode> redisNodes =newArrayList<>();
        addresses.forEach( address -> redisNodes.add(newRedisServer(address.getHost(), address.getPort())));return redisNodes;}privatestaticApplicationContext applicationContext;publicMap<String,Map<String,String>>getRedis(){return redis;}/**
     * 实现ApplicationContextAware接口的context注入函数, 将其存入静态变量.
     */@OverridepublicvoidsetApplicationContext(ApplicationContext applicationContext){CustomRedisConfig.applicationContext = applicationContext;}privatestaticvoidcheckApplicationContext(){if(applicationContext ==null){thrownewIllegalStateException("applicaitonContext未注入,请在applicationContext.xml中定义SpringContextUtil");}}/**
     * 从静态变量ApplicationContext中取得Bean, 自动转型为所赋值对象的类型.
     */@SuppressWarnings("unchecked")publicstatic<T>TgetBean(String name){checkApplicationContext();if(applicationContext.containsBean(name)){return(T) applicationContext.getBean(name);}returnnull;}/**
     * 删除spring中管理的bean
     *
     * @param beanName
     */publicstaticvoidremoveBean(String beanName){DefaultListableBeanFactory acf =(DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
        acf.removeBeanDefinition(beanName);}/**
     * 同步方法注册bean到ApplicationContext中
     *
     * @param beanName
     * @param clazz
     * @param original bean的属性值
     */publicsynchronizedvoidsetBean(String beanName,Class<?> clazz,Map<String,Object> original){checkApplicationContext();DefaultListableBeanFactory beanFactory =(DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();if(beanFactory.containsBean(beanName)){return;}GenericBeanDefinition definition =newGenericBeanDefinition();//类class
        definition.setBeanClass(clazz);if(beanName.startsWith(primaryKey)){
            definition.setPrimary(true);}//属性赋值
        definition.setPropertyValues(newMutablePropertyValues(original));//注册到spring上下文
        beanFactory.registerBeanDefinition(beanName, definition);}publicsynchronizedvoidsetCosBean(String beanName,Class<?> clazz,ConstructorArgumentValues original){checkApplicationContext();DefaultListableBeanFactory beanFactory =(DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();//这里重要if(beanFactory.containsBean(beanName)){return;}GenericBeanDefinition definition =newGenericBeanDefinition();//类class
        definition.setBeanClass(clazz);if(beanName.startsWith(primaryKey)){
            definition.setPrimary(true);}//属性赋值
        definition.setConstructorArgumentValues(newConstructorArgumentValues(original));//注册到spring上下文
        beanFactory.registerBeanDefinition(beanName, definition);}}

2.2. CustomRedisConfigConstant

定义常用配置项的键名

packagecom.iceicepip.project.common.redis;publicclassCustomRedisConfigConstant{privateCustomRedisConfigConstant(){}publicstaticfinalStringREDIS_HOST="host";publicstaticfinalStringREDIS_PORT="port";publicstaticfinalStringREDIS_TIMEOUT="timeout";publicstaticfinalStringREDIS_DATABASE="database";publicstaticfinalStringREDIS_PASSWORD="password";publicstaticfinalStringREDIS_MAXWAITMILLIS="maxWaitMillis";publicstaticfinalStringREDIS_MAXIDLE="maxIdle";publicstaticfinalStringREDIS_MINIDLE="minIdle";publicstaticfinalStringREDIS_MAXTOTAL="maxTotal";publicstaticfinalStringREDIS_TESTONBORROW="testOnBorrow";publicstaticfinalStringREDIS_SENTINEL_MASTER="sentinel.master";publicstaticfinalStringREDIS_SENTINEL_NODES="sentinel.nodes";publicstaticfinalStringREDIS_CLUSTER_NODES="cluster.nodes";publicstaticfinalStringREDIS_CLUSTER_MAX_REDIRECTS="cluster.max-redirects";publicstaticfinalStringBEAN_NAME_SUFFIX="Redis";publicstaticfinalStringINIT_METHOD_NAME="getInit";}

2.3封装一个Redis操作类 CustomRedisService

packagecom.iceicepip.project.common.redis;importcom.alibaba.ttl.TransmittableThreadLocal;importorg.apache.commons.lang3.StringUtils;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.aop.framework.Advised;importorg.springframework.aop.support.AopUtils;importorg.springframework.beans.factory.NoSuchBeanDefinitionException;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.context.ApplicationContext;importorg.springframework.dao.DataAccessException;importorg.springframework.data.redis.connection.RedisConnection;importorg.springframework.data.redis.connection.RedisStringCommands;importorg.springframework.data.redis.connection.ReturnType;importorg.springframework.data.redis.connection.StringRedisConnection;importorg.springframework.data.redis.core.*;importorg.springframework.data.redis.core.types.Expiration;importjavax.annotation.Resource;importjava.nio.charset.StandardCharsets;importjava.util.*;importjava.util.concurrent.TimeUnit;importjava.util.function.Function;importjava.util.function.Supplier;publicclassCustomRedisService{privatestaticfinalLogger logger =LoggerFactory.getLogger(CustomRedisService.class);privateStringRedisTemplate stringRedisTemplate;privateRedisTemplate redisTemplate;@Value("${distribute.lock.MaxSeconds:100}")privateInteger lockMaxSeconds;privatestaticLongLOCK_WAIT_MAX_TIME=120000L;@ResourceprivateApplicationContext applicationContext;/**
     * 保存锁的value
     */privateTransmittableThreadLocal<String> redisLockReentrant =newTransmittableThreadLocal<>();/**
     * 解锁lua脚本
     */privatestaticfinalStringRELEASE_LOCK_SCRIPT="if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";/**
     * redis锁固定前缀
     */privatestaticfinalStringREDIS_LOCK_KEY_PREFIX="xxx:redisLock";/**
     * redis nameSpace
     */privatestaticfinalStringREDIS_NAMESPACE_PREFIX=":";@Value("${spring.application.name}")privateString appName;publicCustomRedisService(){}publicCustomRedisService(StringRedisTemplate stringRedisTemplate,RedisTemplate redisTemplate){this.stringRedisTemplate = stringRedisTemplate;this.redisTemplate = redisTemplate;}publicStringRedisTemplategetStringRedisTemplate(){return stringRedisTemplate;}publicRedisTemplategetRedisTemplate(){return redisTemplate;}//以下是操作publicvoidsaveOrUpdate(HashMap<String,String> values)throwsException{ValueOperations<String,String> valueOps = stringRedisTemplate
                .opsForValue();
        valueOps.multiSet(values);}publicvoidsaveOrUpdate(String key,String value)throwsException{ValueOperations<String,String> valueOps = stringRedisTemplate
                .opsForValue();
        valueOps.set(key, value);}publicStringgetValue(String key)throwsException{ValueOperations<String,String> valueOps = stringRedisTemplate
                .opsForValue();return valueOps.get(key);}publicvoidsetValue(String key,String value)throwsException{ValueOperations<String,String> valueOps = stringRedisTemplate
                .opsForValue();
        valueOps.set(key, value);}publicvoidsetValue(String key,String value,long timeout,TimeUnit unit)throwsException{ValueOperations<String,String> valueOps = stringRedisTemplate
                .opsForValue();
        valueOps.set(key, value, timeout, unit);}publicList<String>getValues(Collection<String> keys)throwsException{ValueOperations<String,String> valueOps = stringRedisTemplate
                .opsForValue();return valueOps.multiGet(keys);}publicvoiddelete(String key)throwsException{
        stringRedisTemplate.delete(key);}publicvoiddelete(Collection<String> keys)throwsException{
        stringRedisTemplate.delete(keys);}publicvoidaddSetValues(String key,String... values)throwsException{SetOperations<String,String> setOps = stringRedisTemplate.opsForSet();
        setOps.add(key, values);}publicSet<String>getSetValues(String key)throwsException{SetOperations<String,String> setOps = stringRedisTemplate.opsForSet();return setOps.members(key);}publicStringgetSetRandomMember(String key)throwsException{SetOperations<String,String> setOps = stringRedisTemplate.opsForSet();return setOps.randomMember(key);}publicvoiddelSetValues(String key,Object... values)throwsException{SetOperations<String,String> setOps = stringRedisTemplate.opsForSet();
        setOps.remove(key, values);}publicLonggetZsetValuesCount(String key)throwsException{return stringRedisTemplate.opsForSet().size(key);}publicvoidaddHashSet(String key,HashMap<String,String> args)throwsException{HashOperations<String,String,String> hashsetOps = stringRedisTemplate
                .opsForHash();
        hashsetOps.putAll(key, args);}publicMap<String,String>getHashSet(String key)throwsException{HashOperations<String,String,String> hashsetOps = stringRedisTemplate
                .opsForHash();return hashsetOps.entries(key);}publicMap<byte[],byte[]>getHashByteSet(String key)throwsException{RedisConnection connection =null;try{
            connection = redisTemplate.getConnectionFactory().getConnection();return connection.hGetAll(key.getBytes());}catch(Exception e){thrownewException(e);}finally{if(Objects.nonNull(connection)&&!connection.isClosed()){
                connection.close();}}}publicList<byte[]>getHashMSet(byte[] key,byte[][] fields)throwsException{return stringRedisTemplate.getConnectionFactory().getConnection().hMGet(key, fields);}/**
     * 设备hash中的值
     *
     * @param key
     * @param field
     * @param vaule
     * @return
     * @throws Exception
     */publicBooleansetHashMSet(byte[] key,byte[] field,byte[] vaule)throwsException{return stringRedisTemplate.getConnectionFactory().getConnection().hSet(key, field, vaule);}/**
     * 采用Pipeline方式获取多个Key的数据
     *
     * @param keys   Key数组
     * @param fields Hash对象的二级Key
     * @return 结果数组,每个Object对象为List<byte[]>,使用时需判断是否为null
     * @throws Exception
     */publicList<Object>getHashMSet(byte[][] keys,byte[][] fields)throwsException{if(keys ==null|| keys.length ==0|| fields ==null|| fields.length ==0){returnnull;}RedisConnection connection = stringRedisTemplate.getConnectionFactory().getConnection();try{
            connection.openPipeline();for(byte[] key : keys){
                connection.hMGet(key, fields);}return connection.closePipeline();}finally{if(!connection.isClosed()){
                connection.close();}}}/**
     * 采用Pipeline方式获取多个Key的数据
     *
     * @param keys  Key数组
     * @param field Hash对象的二级Key
     * @return 结果数组,每个Object对象为List<byte[]>,使用时需判断是否为null
     * @throws Exception
     */publicList<Object>getHashMSet(byte[][] keys,byte[] field)throwsException{if(keys ==null|| keys.length ==0|| field ==null){returnnull;}RedisConnection connection = stringRedisTemplate.getConnectionFactory().getConnection();try{
            connection.openPipeline();for(byte[] key : keys){
                connection.hGet(key, field);}return connection.closePipeline();}finally{if(!connection.isClosed()){
                connection.close();}}}/**
     * 采用Pipeline方式获取多个Key的数据
     *
     * @param keys Key数组
     * @return 结果数组,每个Object对象为List<byte[]>,使用时需判断是否为null
     * @throws Exception
     */publicList<Object>getHashMSet(byte[][] keys)throwsException{if(keys ==null|| keys.length ==0){returnnull;}RedisConnection connection = stringRedisTemplate.getConnectionFactory().getConnection();try{
            connection.openPipeline();for(byte[] key : keys){
                connection.hGetAll(key);}return connection.closePipeline();}finally{if(!connection.isClosed()){
                connection.close();}}}/**
     * 删除批量string
     *
     * @param keys Key数组
     */publicvoiddeleteAllStringValues(byte[][] keys){if(keys ==null|| keys.length ==0){return;}RedisConnection connection = stringRedisTemplate.getConnectionFactory().getConnection();try{
            connection.openPipeline();for(byte[] key : keys){
                connection.del(key);}
            connection.closePipeline();}finally{if(!connection.isClosed()){
                connection.close();}}}publicList<String>getHashMSet(String key,List<String> fields)throwsException{HashOperations<String,String,String> hashsetOps = stringRedisTemplate
                .opsForHash();return hashsetOps.multiGet(key, fields);}publicList<byte[]>getHashByteMSet(String key,byte[]... fields)throwsException{//        HashOperations<String, String, byte[]> hashsetOps = stringRedisTemplate//                .opsForHash();//        return hashsetOps.multiGet(key, fields);RedisConnection connection =null;try{
            connection = redisTemplate.getConnectionFactory().getConnection();return connection.hMGet(key.getBytes(), fields);}catch(Exception e){thrownewException(e);}finally{if(Objects.nonNull(connection)&&!connection.isClosed()){
                connection.close();}}}publicvoiddelHashSetValues(String key,Object... values)throwsException{HashOperations<String,String,String> hashsetOps = stringRedisTemplate
                .opsForHash();
        hashsetOps.delete(key, values);}publicvoidaddZset(String key,String value,double score)throwsException{ZSetOperations<String,String> zSetOps = stringRedisTemplate
                .opsForZSet();
        zSetOps.add(key, value, score);}publicSet<String>getZsetValues(String key)throwsException{returnnull;}publicvoiddelZsetValues(String key,Object... values)throwsException{ZSetOperations<String,String> zSetOps = stringRedisTemplate
                .opsForZSet();
        zSetOps.remove(key, values);}publicStringgetHashByKey(String redisKey,String mapKey)throwsException{HashOperations<String,String,String> hashsetOps = stringRedisTemplate.opsForHash();return hashsetOps.get(redisKey, mapKey);}publicbyte[]getHashByteByKey(String redisKey,String mapKey)throwsException{RedisConnection connection =null;try{
            connection = redisTemplate.getConnectionFactory().getConnection();return connection.hGet(redisKey.getBytes(), mapKey.getBytes());}catch(Exception e){thrownewException(e);}finally{if(Objects.nonNull(connection)&&!connection.isClosed()){
                connection.close();}}//        HashOperations<String, String, byte[]> hashsetOps = stringRedisTemplate.opsForHash();//        return hashsetOps.get(redisKey, mapKey);}publicMap<byte[],byte[]>getHashByte(String redisKey)throwsException{RedisConnection connection =null;try{
            connection = redisTemplate.getConnectionFactory().getConnection();return connection.hGetAll(redisKey.getBytes());}catch(Exception e){thrownewException(e);}finally{if(Objects.nonNull(connection)&&!connection.isClosed()){
                connection.close();}}}publicvoidaddHashSet(String redisKey,String mapKey,String mapValue)throwsException{
        stringRedisTemplate.opsForHash().put(redisKey, mapKey, mapValue);}publicSet<String>getSet(String key)throwsException{SetOperations<String,String> setOperations = stringRedisTemplate.opsForSet();return setOperations.members(key);}publicvoidaddSetValuesPipelined(finalString[] keys,finalString value)throwsException{
        stringRedisTemplate.executePipelined(newRedisCallback<Object>(){@OverridepublicObjectdoInRedis(RedisConnection connection){StringRedisConnection stringRedisConn =(StringRedisConnection) connection;for(int i =0; i < keys.length; i++){
                    stringRedisConn.sAdd(keys[i], value);}//必须返回nullreturnnull;}});}publicvoiddelSetValuesPipelined(finalString[] keys,finalString value)throwsException{
        stringRedisTemplate.executePipelined(newRedisCallback<Object>(){@OverridepublicObjectdoInRedis(RedisConnection connection){StringRedisConnection stringRedisConn =(StringRedisConnection) connection;for(int i =0; i < keys.length; i++){
                    stringRedisConn.sRem(keys[i], value);}//必须返回nullreturnnull;}});}publicvoiddelHashByKey(String redisKey,String mapKey)throwsException{HashOperations<String,String,String> hashMapOps = stringRedisTemplate.opsForHash();
        hashMapOps.delete(redisKey, mapKey);}publicBooleanhasKey(String key)throwsException{return stringRedisTemplate.hasKey(key);}/**
     * 设置用户其他类的缓存
     *
     * @param key
     * @param field   hash结构的field
     * @param data    需要存的数据
     * @param timeOut 超时时间
     * @param unit    时间单位
     */publicvoidsetHashOther(String key,String field,String data,long timeOut,TimeUnit unit){

        stringRedisTemplate.opsForHash().put(key, field, data);
        stringRedisTemplate.expire(key, timeOut, unit);}/**
     * 返回用户的其他缓存
     *
     * @param key
     * @param field hash结构的field
     * @return String
     * @throws Exception
     */publicStringgetHashOther(String key,String field)throwsException{returnthis.getHashByKey(key, field);}/**
     * 2019-2-20 changyandong 新增incr方法,设置过期时间
     *
     * @param key
     * @param delta
     * @param timeout
     * @param unit
     * @return
     */publicLongincrement(finalString key,finalint delta,finallong timeout,finalTimeUnit unit){if(timeout <=0|| unit ==null){return stringRedisTemplate.opsForValue().increment(key, delta);}List<Object> result = stringRedisTemplate
                .executePipelined(newSessionCallback<Object>(){@Overridepublic<K,V>Objectexecute(RedisOperations<K,V> operations)throwsDataAccessException{ValueOperations<K,V> ops = operations.opsForValue();
                        ops.increment((K) key, delta);
                        operations.expire((K) key, timeout, unit);returnnull;}});return(Long) result.get(0);}/**
     * 管道增加hash结构
     */publicvoidaddHashValuesPipelined(Map<String,Map<String,String>> keys){
        stringRedisTemplate.executePipelined((RedisCallback<Object>) connection ->{StringRedisConnection stringRedisConn =(StringRedisConnection) connection;
            keys.forEach(stringRedisConn::hMSet);//必须返回nullreturnnull;});}/**
     * 管道增加hash结构  删除老hash结构
     */publicvoidaddHashValuesPipelinedRemoveOldHash(Map<String,Map<String,String>> keys){
        stringRedisTemplate.executePipelined((RedisCallback<Object>) connection ->{StringRedisConnection stringRedisConn =(StringRedisConnection) connection;
            stringRedisConn.del(keys.keySet().toArray(newString[0]));
            keys.forEach(stringRedisConn::hMSet);//必须返回nullreturnnull;});}/**
     * 分布式锁模板方法
     *
     * @param businessKey       业务key
     * @param callbackFunction 回调方法
     * @param s                回调方法具体入参
     * @param <S>              回调方法入参类型
     * @param <T>              回调方法返回值类型
     * @return 回调方法返回值
     */public<S,T>TredisLockCallback(String businessKey,Function<S,T> callbackFunction,S s){try{redisLock(businessKey);return callbackFunction.apply(s);}finally{redisUnLock(businessKey);}}public<T>TredisLockSupplier(String businessKey,Supplier<T> supplier){returnredisLockSupplier(businessKey, supplier, lockMaxSeconds,LOCK_WAIT_MAX_TIME,TimeUnit.SECONDS);}public<T>TredisLockSupplier(String businessKey,Supplier<T> supplier,long lockMaxTime,long tryTimeout,TimeUnit timeUnit){try{redisLock(businessKey, lockMaxTime, tryTimeout, timeUnit);return supplier.get();}finally{redisUnLock(businessKey);}}/**
     * 获取锁(不等待,直接返回 是否获取到锁资源)
     *
     * @param businessKey 业务key
     * @return 是否获取到锁资源
     */publicbooleanredisLockSuspend(String businessKey){returnredisLockSuspend(businessKey, lockMaxSeconds,TimeUnit.SECONDS);}/**
     *  获取锁(不等待,直接返回 是否获取到锁资源)
     * @param businessKey 业务key
     * @param lockMaxTime 锁占用时长
     * @param timeUnit 时间单位
     * @return 是否获取锁资源
     */publicbooleanredisLockSuspend(String businessKey,long lockMaxTime,TimeUnit timeUnit){String lockKey =generateLockKey(businessKey);long finalLockMaxTime = timeUnit.toMillis(lockMaxTime);//可重入锁判断if(isReentrantLock(lockKey)){returnBoolean.TRUE;}RedisCallback<Boolean> callback =(connection)-> connection.set(
                lockKey.getBytes(StandardCharsets.UTF_8),
                businessKey.getBytes(StandardCharsets.UTF_8),Expiration.milliseconds(finalLockMaxTime),RedisStringCommands.SetOption.SET_IF_ABSENT);return stringRedisTemplate.execute(callback);}/**
     * @param keyPrefix  redis锁 key前缀
     * @param key        key
     * @param tryTimeout 超时时间
     * @param timeUnit   时间单位
     * @return 是否获取到锁资源
     */@DeprecatedpublicbooleanredisLock(String keyPrefix,String key,long lockMaxTime,long tryTimeout,TimeUnit timeUnit){String businessKey =getLockKey(keyPrefix, key);returnredisLock(businessKey, lockMaxTime, tryTimeout, timeUnit);}publicbooleanredisLock(String businessKey,long lockMaxTime,long tryTimeout,TimeUnit timeUnit){
        tryTimeout =System.currentTimeMillis()+ timeUnit.toMillis(tryTimeout);
        lockMaxTime = timeUnit.toMillis(lockMaxTime);returnredisLock(businessKey, lockMaxTime, tryTimeout);}/**
     * 获取redis分布式锁 (默认超时时间)
     *
     * @param keyPrefix redis锁 key前缀
     * @param key       key
     * @return 是否获取到锁资源
     */@DeprecatedpublicbooleanredisLock(String keyPrefix,String key){String businessKey =getLockKey(keyPrefix, key);returnredisLock(businessKey);}publicbooleanredisLock(String businessKey){long endTime =System.currentTimeMillis()+LOCK_WAIT_MAX_TIME;long lockMaxTime =TimeUnit.SECONDS.toMillis(this.lockMaxSeconds);returnredisLock(businessKey, lockMaxTime, endTime);}/**
     * 获取redis分布式锁 (默认超时时间)
     * @param businessKey 业务key
     * @param lockMaxTime 锁占用时长
     * @param endTime 结束时间
     * @return 是否获取到锁资源
     */privatebooleanredisLock(String businessKey,long lockMaxTime,long endTime){String lockKey =generateLockKey(businessKey);
        logger.debug("redisLock businessKey:{},  lockKey:{}, lockMaxTime:{}, endTime:{}", businessKey, lockKey, lockMaxTime, endTime);//可重入锁判断if(isReentrantLock(lockKey)){
            logger.debug("redisLock lockKey:{}, threadName:{}, isReentrantLock true", lockKey,Thread.currentThread().getName());returnBoolean.TRUE;}RedisCallback<Boolean> callback =(connection)-> connection.set(
                lockKey.getBytes(StandardCharsets.UTF_8),
                businessKey.getBytes(StandardCharsets.UTF_8),Expiration.milliseconds(lockMaxTime),RedisStringCommands.SetOption.SET_IF_ABSENT);//在timeout时间内仍未获取到锁,则获取失败while(System.currentTimeMillis()< endTime){if(stringRedisTemplate.execute(callback)){
                redisLockReentrant.set(lockKey);
                logger.debug("redisLock getKey  lockKey:{},  ", lockKey);returntrue;}try{Thread.sleep(100);}catch(InterruptedException e){
                logger.error("获取redis分布式锁出错", e);Thread.currentThread().interrupt();}}
        logger.debug("redisLock meiyoukey  lockKey:{},  ", lockKey);returnfalse;}/**
     * 释放分布式锁
     *
     * @param keyPrefix redis锁 key前缀
     * @param key       key
     */@DeprecatedpublicBooleanredisUnLock(String keyPrefix,String key){String lockKey =getLockKey(keyPrefix, key);returnredisUnLock(lockKey);}publicBooleanredisUnLock(String businessKey){String lockKey =generateLockKey(businessKey);RedisCallback<Boolean> callback =(connection)-> connection.eval(RELEASE_LOCK_SCRIPT.getBytes(),ReturnType.BOOLEAN,1,
                lockKey.getBytes(StandardCharsets.UTF_8),
                businessKey.getBytes(StandardCharsets.UTF_8));//清空 ThreadLocal
        redisLockReentrant.remove();Boolean execute = stringRedisTemplate.execute(callback);
        logger.debug("redisUnLock execute  lockKey:{},  ", lockKey);return execute;}privateStringgetLockKey(String keyPrefix,String key){return keyPrefix +"-"+ key;}/**
     * 是否为重入锁
     */privatebooleanisReentrantLock(String lockKey){String originValue = redisLockReentrant.get();String redisValue = stringRedisTemplate.opsForValue().get(lockKey);returnStringUtils.isNotBlank(originValue)&& originValue.equals(redisValue);}/**
     * 生成规则要求的 key
     *  xxx:redisLock:${appName}:${classSimpleName}:${methodName}:${businessKey}
     * @param businessKey  业务key
     * @return key
     */privateStringgenerateLockKey(String businessKey){StackTraceElement[] stackTrace =Thread.currentThread().getStackTrace();String classSimpleName =StringUtils.EMPTY;String methodName =StringUtils.EMPTY;for(StackTraceElement traceElement : stackTrace){String itemClassName = traceElement.getClassName();//如果是当前类或者stack类 continue;if(itemClassName.equals(this.getClass().getName())|| itemClassName.equals(StackTraceElement.class.getName())){continue;}char[] cs=itemClassName.substring(itemClassName.lastIndexOf(".")+1).toCharArray();
            cs[0]+=32;//一直找,找到被spring管理的类。Object target;try{
                target = applicationContext.getBean(String.valueOf(cs));}catch(NoSuchBeanDefinitionException e){continue;}//如果是代理类,找到实际类if(AopUtils.isAopProxy(target)&& target instanceofAdvised){Advised advised =(Advised) target;try{
                    target = advised.getTargetSource().getTarget();}catch(Exception e){continue;}}if(Objects.nonNull(target)){
                classSimpleName = target.getClass().getSimpleName();
                methodName = traceElement.getMethodName();break;}}returnREDIS_LOCK_KEY_PREFIX.concat(REDIS_NAMESPACE_PREFIX).concat(appName.toLowerCase()).concat(REDIS_NAMESPACE_PREFIX).concat(classSimpleName).concat(REDIS_NAMESPACE_PREFIX).concat(methodName).concat(REDIS_NAMESPACE_PREFIX).concat(businessKey);}}

2.4 将自动配置导入

在这里插入图片描述
在工程目录中创建

common-redis-lettuce/src/main/resources/META-INF/spring

创建文件名为

org.springframework.boot.autoconfigure.AutoConfiguration.imports 

文件
文件内容:

com.iceicepip.project.common.redis.CustomRedisConfig

解释

在工程目录中创建

common-redis-lettuce/src/main/resources/META-INF/spring

目录,并在该目录下创建一个名为

org.springframework.boot.autoconfigure.AutoConfiguration.imports

的文件。该文件的作用是指示 Spring Boot 在自动配置期间需要导入哪些额外的配置类。

org.springframework.boot.autoconfigure.AutoConfiguration.imports

文件中,可以添加需要导入的其他配置类的全限定类名。例如,如果我们需要在自动配置期间导入一个名为

CustomRedisConfig

的配置类,可以在该文件中添加以下内容:

com.iceicepip.project.common.redis.CustomRedisConfig

这样,在应用程序启动时,Spring Boot 会自动加载

CustomRedisConfig

类,并将其与自动配置合并,以提供完整的应用程序配置。

3. 使用方式

其中

xxx 

为在Spring Boot 配置文件中配置的多数据源的标识.如’user’、“iot”

@Autowired@Qualifier("xxxRedis")privateCustomRedisService xxxRedisService;@Autowired@Qualifier("userRedis")privateCustomRedisService userRedisService;

或者直接使用RedisTemplate 。

@Autowired@Qualifier("userRedisTemplate")privateRedisTemplate  userRedisTemplate;@Autowired@Qualifier("xxxStringRedisTemplate")privateStringRedisTemplate  xxxStringRedisTemplate;@Autowired@Qualifier("xxxRedisTemplate")privateRedisTemplate   xxxRedisTemplate;

4. 源码地址

https://github.com/wangshuai67/Redis-Tutorial-2023

5. Redis从入门到精通系列文章

  • 《SpringBoot Redis 使用Lettuce和Jedis配置哨兵模式》
  • 《Redis【应用篇】之RedisTemplate基本操作》
  • 《Redis 从入门到精通【实践篇】之SpringBoot配置Redis多数据源》
  • 《Redis 从入门到精通【进阶篇】之三分钟了解Redis HyperLogLog 数据结构》
  • 《Redis 从入门到精通【进阶篇】之三分钟了解Redis地理位置数据结构GeoHash》
  • 《Redis 从入门到精通【进阶篇】之高可用哨兵机制(Redis Sentinel)详解》
  • 《Redis 从入门到精通【进阶篇】之redis主从复制详解》
  • 《Redis 从入门到精通【进阶篇】之Redis事务详解》
  • 《Redis从入门到精通【进阶篇】之对象机制详解》
  • 《Redis从入门到精通【进阶篇】之消息传递发布订阅模式详解》
  • 《Redis从入门到精通【进阶篇】之持久化 AOF详解》
  • 《Redis从入门到精通【进阶篇】之持久化RDB详解》
  • 《Redis从入门到精通【高阶篇】之底层数据结构字典(Dictionary)详解》
  • 《Redis从入门到精通【高阶篇】之底层数据结构快表QuickList详解》
  • 《Redis从入门到精通【高阶篇】之底层数据结构简单动态字符串(SDS)详解》
  • 《Redis从入门到精通【高阶篇】之底层数据结构压缩列表(ZipList)详解》
  • 《Redis从入门到精通【进阶篇】之数据类型Stream详解和使用示例》在这里插入图片描述大家好,我是冰点,今天的Redis【实践篇】之SpringBoot Redis 多数据源集成支持哨兵模式和Cluster集群模式,全部内容就是这些。如果你有疑问或见解可以在评论区留言。

本文转载自: https://blog.csdn.net/wangshuai6707/article/details/131924586
版权归原作者 冰点. 所有, 如有侵权,请联系我们删除。

“SpringBoot Redis 多数据源集成支持哨兵模式和Cluster集群模式”的评论:

还没有评论