一、前言
在前面我们通过以下章节对Redis
使用有了基础的了解:
Spring Boot实现Redis同数据源动态切换DB | Spring Cloud 31
此章节基于spring-boot-starter-data-redis
模块,实现了Redis
多数据源动态切换,具体功能如下:
- 突破一个项目只能一个连接
Redis
数据源的限制 - 提供多种操作切换
Redis
数据源的方式(@Autowired
方式 和RedisSelectSupport
方式) - 提供完善的代码使用示例
二、项目结构
-
RedisSelect
:自定义注解,用于标注要切换的Redis
数据源 -
RedisSelectSupport
:自定义多数据源切换支持,用于线程间传递多数据源标识 -
MultiRedisProperties
:获取配置文件中Redis
多数据源属性定义 -
RedisInstanceSelectAspect
:自定义AOP
切面,对RedisSelect
注解方法进行拦截,调用RedisSelectSupport
,利用RedisSelectSupport
设置数据源标识
-
MultiRedisLettuceConnectionFactory
:自定义Redis
数据源的接口,利用RedisSelectSupport
获取Redis
数据源标识,实现多数据源切换 -
RedisInstanceConfig
:实现MultiRedisLettuceConnectionFactory
定义,读取MultiRedisProperties
属性实现各数据源的LettuceConnectionFactory
,将多数据源组装为Map
下文对各类的功能描述不进行重复介绍,请详细了解本章节。
本示例源码位于项目的
redis/redis-multi-instance
模块下。
三、动态切换Redis多数据源实现
3.1 所需依赖
redis/redis-multi-instance/pom.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>redis</artifactId><groupId>com.gm</groupId><version>0.0.1-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>redis-multi-instance</artifactId><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.83</version></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId></dependency></dependencies></project>
3.2 配置文件
src/main/resources/application.yml
:
server:port: 6009spring:application:name: @artifactId@redis:database: 1timeout: 60000lettuce:pool:max-active: -1max-idle: -1max-wait: -1min-idle: -1enable-multi: truemulti:default:database: 1host: password: port: 6379timeout: 60000lettuce:pool:max-active: -1max-idle: -1max-wait: -1min-idle: -1enable-multi: truetwo:database: 1host: port: 6379password: timeout: 60000lettuce:pool:max-active: -1max-idle: -1max-wait: -1min-idle: -1logging:level:org:springframework:boot:autoconfigure:logging: info
3.3 自定义注解类@RedisSelect
com/gm/multi/redis/config/select/RedisSelectSupport.java
:
import java.lang.annotation.*;/*** 注解,用于切换不同数据源的redis*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisSelect {/*** redis库 0 - 15 库** @return*/String instance() default "default";
}
3.4 多数据源属性读取MultiRedisProperties
com/gm/multi/redis/config/select/instance/MultiRedisProperties.java
:
import lombok.Data;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Map;@Data
@Component
@ConfigurationProperties(prefix = "spring.redis")
public class MultiRedisProperties {/*** 默认连接必须配置,配置 key 为 default*/public static final String DEFAULT = "default";private boolean enableMulti = false;private Map<String, RedisProperties> multi;
}
3.5 自定义切面类RedisInstanceSelectAspect
com/gm/multi/redis/config/select/aspect/RedisInstanceSelectAspect.java
:
import com.gm.multi.redis.config.select.RedisSelectSupport;
import com.gm.multi.redis.config.select.annotation.RedisSelect;
import com.gm.multi.redis.config.select.instance.MultiRedisLettuceConnectionFactory;
import com.gm.multi.redis.config.select.instance.MultiRedisProperties;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;@Slf4j
@Aspect
@Component
public class RedisInstanceSelectAspect {private final String defaultInstance = MultiRedisProperties.DEFAULT;/*** 创建RedisSelect对应的切面,来对标有注解的方法拦截** @param point* @return* @throws Throwable*/@Around("@annotation(com.gm.multi.redis.config.select.annotation.RedisSelect)")@ConditionalOnBean(MultiRedisLettuceConnectionFactory.class)public Object configRedis(ProceedingJoinPoint point) throws Throwable {String instance = defaultInstance;try {MethodSignature signature = (MethodSignature) point.getSignature();Method method = signature.getMethod();RedisSelect config = method.getAnnotation(RedisSelect.class);if (config != null) {instance = config.instance();}RedisSelectSupport.selectInstance(instance);return point.proceed();} finally {RedisSelectSupport.selectInstance(defaultInstance);log.debug("redis instance reset {} to {}", instance, defaultInstance);}}
}
3.6 自定义数据源MultiRedisLettuceConnectionFactory
com/gm/multi/redis/config/select/instance/MultiRedisLettuceConnectionFactory.java
:
import com.gm.multi.redis.config.select.RedisSelectSupport;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.*;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.util.StringUtils;import java.util.Map;/*** 自定义数据源,实现多数据源切换,利用RedisSelectSupport获取Redis数据源标识,实现多数据源切换*/
public class MultiRedisLettuceConnectionFactoryimplements InitializingBean, DisposableBean, RedisConnectionFactory, ReactiveRedisConnectionFactory {public Map<String, LettuceConnectionFactory> getConnectionFactoryMap() {return connectionFactoryMap;}private final Map<String, LettuceConnectionFactory> connectionFactoryMap;public MultiRedisLettuceConnectionFactory(Map<String, LettuceConnectionFactory> connectionFactoryMap) {this.connectionFactoryMap = connectionFactoryMap;}@Overridepublic void destroy() {connectionFactoryMap.values().forEach(LettuceConnectionFactory::destroy);}@Overridepublic void afterPropertiesSet() {connectionFactoryMap.values().forEach(LettuceConnectionFactory::afterPropertiesSet);}private LettuceConnectionFactory currentLettuceConnectionFactory() {String instance = RedisSelectSupport.getSelectInstance();if (StringUtils.hasLength(instance)) {LettuceConnectionFactory factory = connectionFactoryMap.get(instance);return factory;}return connectionFactoryMap.get(MultiRedisProperties.DEFAULT);}@Overridepublic ReactiveRedisConnection getReactiveConnection() {return currentLettuceConnectionFactory().getReactiveConnection();}@Overridepublic ReactiveRedisClusterConnection getReactiveClusterConnection() {return currentLettuceConnectionFactory().getReactiveClusterConnection();}@Overridepublic RedisConnection getConnection() {return currentLettuceConnectionFactory().getConnection();}@Overridepublic RedisClusterConnection getClusterConnection() {return currentLettuceConnectionFactory().getClusterConnection();}@Overridepublic boolean getConvertPipelineAndTxResults() {return currentLettuceConnectionFactory().getConvertPipelineAndTxResults();}@Overridepublic RedisSentinelConnection getSentinelConnection() {return currentLettuceConnectionFactory().getSentinelConnection();}@Overridepublic DataAccessException translateExceptionIfPossible(RuntimeException ex) {return currentLettuceConnectionFactory().translateExceptionIfPossible(ex);}
}
3.7 自定义配置类RRedisInstanceConfig
com/gm/multi/redis/config/select/instance/RedisInstanceConfig.java
:
import io.lettuce.core.resource.ClientResources;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
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.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConfiguration;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import java.util.HashMap;
import java.util.Map;@ConditionalOnProperty(prefix = "spring.redis", value = "enable-multi", matchIfMissing = false)
@Configuration(proxyBeanMethods = false)
public class RedisInstanceConfig {@Beanpublic MultiRedisLettuceConnectionFactory multiRedisLettuceConnectionFactory(ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers, ClientResources clientResources, MultiRedisProperties multiRedisProperties, ObjectProvider<RedisSentinelConfiguration> sentinelConfigurationProvider, ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider) {//读取配置Map<String, LettuceConnectionFactory> connectionFactoryMap = new HashMap<>();Map<String, RedisProperties> multi = multiRedisProperties.getMulti();multi.forEach((k, v) -> {LettuceConnectionFactory lettuceConnectionFactory = lettuceConnectionFactory(v);connectionFactoryMap.put(k, lettuceConnectionFactory);});return new MultiRedisLettuceConnectionFactory(connectionFactoryMap);}public LettuceConnectionFactory lettuceConnectionFactory(RedisProperties redisProperties) {RedisStandaloneConfiguration redisConfiguration = new RedisStandaloneConfiguration(redisProperties.getHost(), redisProperties.getPort());// 设置选用的数据库号码redisConfiguration.setDatabase(redisProperties.getDatabase());// 设置 redis 数据库密码redisConfiguration.setPassword(redisProperties.getPassword());// 根据配置和客户端配置创建连接LettuceConnectionFactory factory = new LettuceConnectionFactory((RedisConfiguration) redisConfiguration);return factory;}
}
四、测试业务搭建
4.1 多种操作切换Redis数据源的方式
支持原生的
RedisTemplate
或者ReactiveRedisTemplate
4.1.1 注解@RedisSelect方式
com/gm/multi/redis/controller/RedisInstanceSelectController.java
:
@RequestMapping("/one")@RedisSelect() //选择默认库public String selectOne() {redisTemplate.opsForValue().set("one", "one_" + System.currentTimeMillis());String one = redisTemplate.opsForValue().get("one");return one;}@RequestMapping("/two")@RedisSelect(instance = "two") //选择two库public String selectTwo() {redisTemplate.opsForValue().set("two", "two_" + System.currentTimeMillis());String two = redisTemplate.opsForValue().get("two");return two;}
4.1.2 设置RedisSelectSupport方式
/*** 同一个方法中切换不同的redis数据源** @return*/@RequestMapping("/three")@RedisSelectpublic String selectThree() {String one = redisTemplate.opsForValue().get("one");log.info(one);RedisSelectSupport.selectInstance("two");//此处切换到two库String two = redisTemplate.opsForValue().get("two");log.info(two);return two;}
4.2 多线程测试
com/gm/multi/redis/controller/RedisInstanceSelectController.java
:
@AutowiredRedisMultiInstanceService redisMultiInstanceService;@RequestMapping("/testMultiInstance")public String testMultiIndex() {Thread thread[] = new Thread[500];AtomicBoolean result = new AtomicBoolean(true);for (int i = 0; i < thread.length; i++) {int finalI = i;thread[i] = new Thread(() -> {try {redisMultiInstanceService.testMultiInstance("Thread-" + finalI);} catch (Exception e) {e.printStackTrace();result.set(false);}});thread[i].setName("Thread-" + i);}for (int i = 0; i < thread.length; i++) {thread[i].start();}return "";}
com/gm/multi/redis/service/RedisMultiInstanceService.java
:
public interface RedisMultiInstanceService {void testMultiInstance(String suffix);
}
com/gm/multi/redis/service/impl/RedisMultiInstanceServiceImpl.java
:
import com.gm.multi.redis.config.select.RedisSelectSupport;
import com.gm.multi.redis.service.RedisMultiInstanceService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;@Slf4j
@Service
public class RedisMultiInstanceServiceImpl implements RedisMultiInstanceService {@Autowiredprivate StringRedisTemplate redisTemplate;public void testMultiInstance(String suffix) {String defaultkv = "default-" + suffix + "-instance";String twokv = "two-" + suffix + "-instance";//使用默认连接redisTemplate.opsForValue().set(defaultkv, defaultkv);//使用 two 连接RedisSelectSupport.selectInstance("two");redisTemplate.opsForValue().set(twokv, twokv);//使用默认连接RedisSelectSupport.selectInstance("default");String value1 = String.valueOf(redisTemplate.opsForValue().get(defaultkv));//使用 two 连接RedisSelectSupport.selectInstance("two");String value2 = String.valueOf(redisTemplate.opsForValue().get(twokv));log.info("suffix:{} default={} two={}", suffix, (defaultkv).equals(value1), (twokv).equals(value2));}
}
4.3 测试效果
浏览器访问:http://127.0.0.1:6009/instance/index/one
浏览器访问:http://127.0.0.1:6009/instance/index/two
浏览器访问:http://127.0.0.1:6009/redis/instance/testMultiInstance
default
数据源
two
数据源
查看控制台输出暂未发现在切换Redis数据源在多线程下,存在线程安全问题。
查看Redis
的client
信息:
default
数据源
two
数据源
五、源码
源码地址:https://gitee.com/gm19900510/springboot-cloud-example.git 中模块redis/redis-multi-instance
。