Cing
发布于 2021-10-31 / 292 阅读
0

Spring Data Redis 多源

Spring Data Redis 多源

完整代码:Ciiiiing/springboot_multi_redis

最近需要在同一个项目中访问多个 redisspring data redis 默认是只支持一个数据源的,那就需要我们自己改造

网上搜了一些文章,大多有一些错漏,并且只给出了结果(还是错的)没有为什么,所以自己研究了一下,分享一下过程

首先在一个 spring boot 项目的 pom 文件中引入 spring data redis

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

yml 做如下配置,host 换成你自己的 redis 服务器地址

spring:
  redis:
    host: 192.168.253.132
    port: 6379
    database: 0

这样我们就可以使用 spring data redis 提供的 bean 来访问 redis 了,我们不妨先来试一试

@SpringBootApplication
public class SpringbootMultiRedisApplication implements CommandLineRunner {
    private final StringRedisTemplate stringRedisTemplate;

    @Autowired
    public SpringbootMultiRedisApplication(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public static void main(String[] args) {
        SpringApplication.run(SpringbootMultiRedisApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        stringRedisTemplate.opsForValue().set("test", "1");
    }
}

先用 RDM 看看数据

image20210607213108560.png

运行一下我们的程序

image20210607213205561.png

好的,成功插入了,至此我们已经成功集成了单源 redis 接下来我们看看怎么实现多源

我们要知道 spring boot 之所以能完成自动配置是因为有很多 autoConfiguration 类来帮我们做了初始化,那我们就来搜一搜 redis 的自动配置类

image20210607213443297.png

ok,我们已经发现确实存在 redis 的自动配置类,我们进入看看

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean(name = "redisTemplate")
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
	public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
		RedisTemplate<Object, Object> template = new RedisTemplate<>();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
		StringRedisTemplate template = new StringRedisTemplate();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

}

嗯,我们能看到 redis 的自动配置类还是比较简单的,大部分功能基本都靠注解来完成,那我们就一个个的来看看这些注解的作用

第一个 @Configuration,点进去就能看到官方对他的说明

Indicates that a class declares one or more @Bean methods and may be processed by the Spring container to generate bean definitions and service requests for those beans at runtime

表示一个类声明了一个或多个 @Bean 方法,并可由 Spring 容器处理,以便在运行时为这些 Bean 生成 Bean 定义和服务请求

第二个 @ConditionalOnClass

@Conditional that only matches when the specified classes are on the classpath.

在指定的类存在于 classpath 时加载

第三个 @EnableConfigurationProperties

Enable support for @ConfigurationProperties annotated beans. @ConfigurationProperties beans can be registered in the standard way (for example using @Bean methods) or, for convenience, can be specified directly on this annotation.

启用对 @ConfigurationProperties 注释的 bean 的支持。@ConfigurationProperties beans 可以以标准方式注册(例如使用 @Bean 方法),或者为了方便,可以直接在这个注解上指定

第四个 @Import

Indicates one or more component classes to import — typically @Configuration classes.

表示要导入的一个或多个组件类 -- 通常是 @Configuration 类

那么那这个注解就是说

  1. RedisAutoConfiguration 这个类是个配置类,用于定义一个或多个 bean
  2. classpath 下存在 RedisOperations.class 这个类则加载它
  3. 启用 RedisProperties.class 该配置类
  4. 导入配置类 LettuceConnectionConfiguration.classJedisConnectionConfiguration.class

点进 RedisProperties.class 这里类里边看看

@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {
    ...
}

就是这个类指定了我们配置文件的格式(我们之前写的 yml

继续看剩下的注解

	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
		StringRedisTemplate template = new StringRedisTemplate();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

第一个 @Bean

Indicates that a method produces a bean to be managed by the Spring container

表明这个方法用于生产一个归 spring 管理的 bean 对象

第二个 @ConditionalOnMissingBean

@Conditional that only matches when no beans meeting the specified requirements are already contained in the BeanFactory. None of the requirements must be met for the condition to match and the requirements do not have to be met by the same bean.

@Conditional,只有在 BeanFactory 中已经没有满足指定要求的 Bean 时才会匹配。条件必须不满足任何要求才能匹配,而且这些要求不一定要由同一个 Bean 来满足

第三个 @ConditionalOnSingleCandidate

@Conditional that only matches when a bean of the specified class is already contained in the BeanFactory and a single candidate can be determined.

@Conditional,只有在 BeanFactory 中已经包含了指定类别的 Bean 并且可以确定一个候选者时才会匹配

The condition will also match if multiple matching bean instances are already contained in the BeanFactory but a primary candidate has been defined;

如果 BeanFactory 中已经包含了多个匹配的 Bean 实例,但已经定义了一个 primary(主要) 的候选者,那么该条件也将匹配

ok,那我们来理解一下是什么意思

  1. 表明 redisTemplate 这个方法用于生成一个由 spring 管理的 bean
  2. 若没有 redisTemplate 这个 bean 则加载 redisTemplate
  3. 若容器中只有一个 RedisConnectionFactory.class

好的,总体梳理一下,我们之所以只需要在 yml 文件中写好 redis 地址就能注入一个 StringRedisTemplate 对象来直接使用,是因为 RedisAutoConfiguration 这个自动配置类帮我们做了初始化

流程大概清楚了,该如何自定义数据源呢?

再看看这段代码

	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
	public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
		StringRedisTemplate template = new StringRedisTemplate();
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}

注意到 RedisConnectionFactory 根据命名我们猜测这是一个工厂类,回忆一下设计模式,工厂模式 用于初始化复杂的对象,ok 那应该就是在这里做了初始化,那么这个工厂类又是哪来的呢?

还记得之前的配置类吗?

@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })

我们使用的是 Lettuce 作为客户端,所以去看看 LettuceConnectionConfiguration.class

    @Bean
	@ConditionalOnMissingBean(RedisConnectionFactory.class)
	LettuceConnectionFactory redisConnectionFactory(
			ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,
			ClientResources clientResources) {
		LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(builderCustomizers, clientResources,
				getProperties().getLettuce().getPool());
		return createLettuceConnectionFactory(clientConfig);
	}

那么能看到是这里注入了 redisConnectionFactory,跟进这个方法

	private LettuceConnectionFactory createLettuceConnectionFactory(LettuceClientConfiguration clientConfiguration) {
		if (getSentinelConfig() != null) {
			return new LettuceConnectionFactory(getSentinelConfig(), clientConfiguration);
		}
		if (getClusterConfiguration() != null) {
			return new LettuceConnectionFactory(getClusterConfiguration(), clientConfiguration);
		}
		return new LettuceConnectionFactory(getStandaloneConfig(), clientConfiguration);
	}

我们使用的是单点 redis 所以再看看 getStandaloneConfig() 这个方法

	protected final RedisStandaloneConfiguration getStandaloneConfig() {
		RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
		if (StringUtils.hasText(this.properties.getUrl())) {
			ConnectionInfo connectionInfo = parseUrl(this.properties.getUrl());
			config.setHostName(connectionInfo.getHostName());
			config.setPort(connectionInfo.getPort());
			config.setUsername(connectionInfo.getUsername());
			config.setPassword(RedisPassword.of(connectionInfo.getPassword()));
		}
		else {
			config.setHostName(this.properties.getHost());
			config.setPort(this.properties.getPort());
			config.setUsername(this.properties.getUsername());
			config.setPassword(RedisPassword.of(this.properties.getPassword()));
		}
		config.setDatabase(this.properties.getDatabase());
		return config;
	}

nice,可以看到实在这里读取了配置文件

我们只要仿照这个建立自己的工厂类就可以了,让我们动手试试

image20210607224326206.png

建立一个 RedisConfig 来创建我们自己的工厂

@Configuration
public class RedisConfig {
    @Bean
    public LettuceConnectionFactory firstFactory() {
        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration("192.168.253.132", 6379);
        configuration.setDatabase(1);
        return new LettuceConnectionFactory(configuration);
    }

    @Bean
    StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}

先建立一个工厂,给 db1 写入数据试试看

image20210607224934448.png

好的没有问题,我们接下来建立第二个工厂并指定给 db2 插入数据

@Configuration
public class RedisConfig {
    @Bean
    public LettuceConnectionFactory firstFactory() {
        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration("192.168.253.132", 6379);
        configuration.setDatabase(1);
        return new LettuceConnectionFactory(configuration);
    }

    @Bean
    public LettuceConnectionFactory secondFactory() {
        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration("192.168.253.132", 6379);
        configuration.setDatabase(2);
        return new LettuceConnectionFactory(configuration);
    }

    @Bean
    StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    StringRedisTemplate stringRedisTemplate2(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}

好,我们运行看看

Parameter 0 of method stringRedisTemplate in com.example.springboot_multi_redis.config.RedisConfig required a single bean, but 2 were found:
	- firstFactory: defined by method 'firstFactory' in class path resource [com/example/springboot_multi_redis/config/RedisConfig.class]
	- secondFactory: defined by method 'secondFactory' in class path resource [com/example/springboot_multi_redis/config/RedisConfig.class]

很遗憾,我们看到程序报错了,这是为什么呢?

通过报错信息 required a single bean, but 2 were found 可以看到有一个地方需要一个 bean 但程序发现了两个不知道该使用哪一个,看到这里,你想起我们之前说的 @ConditionalOnSingleCandidate 这个注解了吗?从官方说明中我们知道,需要一个 primary 就可以了,嘿,这里我们用 @Primary 注解就好

    @Bean
    @Primary
    public LettuceConnectionFactory firstFactory() {
        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration("192.168.253.132", 6379);
        configuration.setDatabase(1);
        return new LettuceConnectionFactory(configuration);
    }

再次运行程序(记得每次重新运行前清掉 redis 的内容,或者每次使用不同的 key 方便你判断是否成功)

image20210607225643242.png

ok,我们的代码是 stringRedisTemplate.opsForValue().set("test", "1"); 所以插入 db1 是符合预期的

现在我们想用 stringRedisTemplate2 来操作 db2,但细心的我们发现

    @Bean
    StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    StringRedisTemplate stringRedisTemplate2(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

这两段代码除了函数名以外完全一致,怎么能使用不同的工厂呢?

这就要用到另一个注解 @Qualifier

This annotation may be used on a field or parameter as a qualifier for candidate beans when autowiring.

用于在自动注入时指定使用的 bean,也就是 by type 的注入,@Autowired 默认是 by name

让我们加入这个注解

    @Bean
    StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    StringRedisTemplate stringRedisTemplate2(@Qualifier("secondFactory") RedisConnectionFactory redisConnectionFactory) {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

修改一下主类

@SpringBootApplication
public class SpringbootMultiRedisApplication implements CommandLineRunner {
    private final StringRedisTemplate stringRedisTemplate;
    private final StringRedisTemplate stringRedisTemplate2;

    @Autowired
    public SpringbootMultiRedisApplication(StringRedisTemplate stringRedisTemplate,
                                           StringRedisTemplate stringRedisTemplate2) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.stringRedisTemplate2 = stringRedisTemplate2;
    }

    public static void main(String[] args) {
        SpringApplication.run(SpringbootMultiRedisApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        stringRedisTemplate.opsForValue().set("test", "1");
        stringRedisTemplate2.opsForValue().set("test", "2");
    }
}

运行程序

image20210607230606369.png

好的,大功告成,我们可以通过同样的方法加入更多的 redis

这篇博客也就此结束了~~

祝大家学习愉快