springboot2.0整合redis(使用Jedis)及自动装配部分源码解析

1.springboot 目前的推荐版是2.1.2.RELEASE,我们就以当前最新的推荐版为例;

2.关于redis在linux下安装,可以参考《linux下安装redis》  《linux下redis集群搭建》

3.spring-data-redis 目前spring的官网的版本是2.1.5,详细查看https://spring.io/projects/spring-data-redis#overview

关于官网描述的特性,主要的几点如下:
1.支持jedis和lettuce,不支持JRedis和SRP

   特别介绍下在2.x版本中,默认是使用lettuce;1.x版本的时候,默认使用的就是Jedis;关于两个的区别:

  1. # Jedis和Lettuce都是Redis Client
  2. # Jedis 是直连模式,在多个线程间共享一个 Jedis 实例时是线程不安全的。
  3. # 如果想要在多线程环境下使用 Jedis,需要使用连接池。
  4. # 每个线程都去拿Jedis 实例,当连接数量增多时,物理连接成本就较高了。
  5. # Lettuce 是基于 netty 的,netty 是一个多线程、事件驱动的 I/O 框架,连接实例可以在多个线程间共享,通过异步的方式可以让我们更好的利用系统资源,而不用浪费线程等待网络或磁盘I/O。

2.异常的处理转换
3.RedisTemplate提供的是redis操作的高级抽象
4.Pubsub 支持
5.redis的哨兵模式以及集群模式的支持
6.JDK, String, JSON and Spring Object/XML的序列化映射
……………………后面的就不翻译了,英语也不太好,翻译的也不太好………………

4.springboot2.x整合redis;主要是使用Jedis的方式,毕竟1.x版本的时候,默认使用的就是Jedis;

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.2.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
 <properties>
        <java.version>1.8</java.version>
        <!-- 默认为2.9.1版本,运行的时候,会报错,指定低一个版本的 -->
        <jedis.version>2.9.0</jedis.version>
    </properties>
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <!-- 默认指定了 lettuce,我们需要排除,并且引入jedis的客户端-->
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

yml文件的配置

spring:
  redis:
    database: 0
    host: localhost
    port: 6379
    timeout: 
      nano: 2
      seconds: 2
    jedis:
      pool:
        max-active: 8
        max-wait: -1
        max-idle: 8
        min-idle: 0

查看redis的配置类org.springframework.boot.autoconfigure.data.redis.RedisProperties,与springboot1.x不同的是
timeout属性由之前的int类型,换成了java8的类型;(下部分,会用源码的形式,查看timeout的类型的改变)
Jedis与Lettuce的连接属性进行了区别;

如果想使用Lettuce,进行整合,只需要把引入spring-boot-starter-data-redis,默认就是Lettuce,只需要把连接池属性改成Lettuce;

如果是集群搭建,配置文件如下

spring:
  redis:
    cluster:
      nodes:
      - 192.168.112.129:7000
      - 192.168.112.129:7001
      - 192.168.112.129:7002
      - 192.168.112.129:7003
      - 192.168.112.129:7004
      - 192.168.112.129:7005
      max-redirects: 3
    jedis:
      pool:
        max-active: 8
        max-wait: -1
        max-idle: 8
        min-idle: 0

5.测试,以及测试结果就不演示了,很简单的案例

在测试过程中,使用redis的客户端(界面化工具),直接获取数据,会遇到序列化的问题;在org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration自动装配类中,实例化了redisTemplate和stringRedisTemplate;

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

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

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

}

我们简单查看源码,就可以看到序列化的问题;

先看RedisTemplate<Object, Object> template = new RedisTemplate<>();的实例化,RedisTemplate构造方法下的,重写父级的public void afterPropertiesSet()方法,直接查看父级的org.springframework.data.redis.core.RedisAccessor类,可以看到afterPropertiesSet()方法,实现于spring的org.springframework.beans.factory.InitializingBean接口,关于InitializingBean接口作用,以及什么时机执行,大家自行去了解

	/*
	 * (non-Javadoc)
	 * @see org.springframework.data.redis.core.RedisAccessor#afterPropertiesSet()
	 */
	@Override
	public void afterPropertiesSet() {

		super.afterPropertiesSet();

		boolean defaultUsed = false;

		if (defaultSerializer == null) {

			defaultSerializer = new JdkSerializationRedisSerializer(
					classLoader != null ? classLoader : this.getClass().getClassLoader());
		}

		if (enableDefaultSerializer) {

			if (keySerializer == null) {
				keySerializer = defaultSerializer;
				defaultUsed = true;
			}
			if (valueSerializer == null) {
				valueSerializer = defaultSerializer;
				defaultUsed = true;
			}
			if (hashKeySerializer == null) {
				hashKeySerializer = defaultSerializer;
				defaultUsed = true;
			}
			if (hashValueSerializer == null) {
				hashValueSerializer = defaultSerializer;
				defaultUsed = true;
			}
		}

		if (enableDefaultSerializer && defaultUsed) {
			Assert.notNull(defaultSerializer, "default serializer null and not all serializers initialized");
		}

		if (scriptExecutor == null) {
			this.scriptExecutor = new DefaultScriptExecutor<>(this);
		}

		initialized = true;
	}

 看代码,就可以看到,在没有指定keySerializer,valueSerializer的情况下,默认使用的是defaultSerializer;

我们在看StringRedisTemplate template = new StringRedisTemplate();构造方法,相信大家也都能看懂,调用了set的方法,把序列化的类,直接使用string的形式

public StringRedisTemplate() {
		setKeySerializer(RedisSerializer.string());
		setValueSerializer(RedisSerializer.string());
		setHashKeySerializer(RedisSerializer.string());
		setHashValueSerializer(RedisSerializer.string());
	}

 所有说,我们对序列化以及反序列化的时候,一般情况下,json串-->POJO对象转换的时候,自己手动转换,可以直接使用StringRedisTemplate;而需要自动转换的时候,可以自定义一个RedisTemplate的实例,使用Jackson2JsonRedisSerializer的形式,序列化valueSerializer,代码如下:

我把key序列了字符换,val用的jackson2JsonRedisSerializer序列化,大家可以根据实际情况来操作;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(
                Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setKeySerializer(RedisSerializer.string());
//        template.setValueSerializer(RedisSerializer.string());
//        template.setHashKeySerializer(RedisSerializer.string());
//        template.setHashValueSerializer(RedisSerializer.string());
        return template;
    }
}

测试代码如下,比较简单,大家自行测试即可 

@RunWith(SpringRunner.class)
@SpringBootTest
public class BadgerRedisSingleApplicationTests {
    @Autowired
    RedisTemplate<String, Object> redisTemplate;

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Test
    public void testRedisTemplate() {
        final String key = "obj";
        List<String> strs = Arrays.asList("d", "b", "a", "c", "a");
        ValueOperations<String, Object> opsForValue = redisTemplate.opsForValue();
        opsForValue.set(key, strs);
        Object str = opsForValue.get(key);
        System.out.println(str);
    }

    @Test
    public void testString() {
        final String key = "test";
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        opsForValue.set(key, "test");
        String str = opsForValue.get(key);
        System.out.println(str);
    }
}

至此,redis的整合就完成了,下面对源码进行讲解下,不感兴趣的同学,可以略过了………………

6.自动装配的源码解析

我们先看org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration自动装配类

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

里面的内容,上面已经介绍过了,我们直接看Import导入的JedisConnectionConfiguration的实例,我们使用的Jedis,排除了Lettuce,所以只有JedisConnectionConfiguration可以实例化成功

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

JedisConnectionConfiguration(RedisProperties properties,
			ObjectProvider<RedisSentinelConfiguration> sentinelConfiguration,
			ObjectProvider<RedisClusterConfiguration> clusterConfiguration,
			ObjectProvider<JedisClientConfigurationBuilderCustomizer> builderCustomizers) {
		super(properties, sentinelConfiguration, clusterConfiguration);
		this.properties = properties;
		this.builderCustomizers = builderCustomizers;
	}

	@Bean
	@ConditionalOnMissingBean(RedisConnectionFactory.class)
	public JedisConnectionFactory redisConnectionFactory() throws UnknownHostException {
		return createJedisConnectionFactory();
	}

我们先看构造方法,实例的时候自动注入了以下属性,并把properties和builderCustomizers存起来了

RedisProperties properties,:这个就yml配置文件里的属性的类
ObjectProvider<RedisSentinelConfiguration> sentinelConfiguration:这个是哨兵模式的配置
ObjectProvider<RedisClusterConfiguration> clusterConfiguration:这个是集群模式的配置
ObjectProvider<JedisClientConfigurationBuilderCustomizer> builderCustomizers:这个是普通模式的构建器

再看redisConnectionFactory()方法,也是这个类中最重要的部分,为RedisAutoConfiguration自动装配的时候,提供的RedisConnectionFactory 这个连接工厂;

private JedisConnectionFactory createJedisConnectionFactory() {
		//获取jedis客户端的配置
		JedisClientConfiguration clientConfiguration = getJedisClientConfiguration();
		//创建哨兵模式的连接工厂(yml只配置了普通模式,此处不判断不生效)
		if (getSentinelConfig() != null) {
			return new JedisConnectionFactory(getSentinelConfig(), clientConfiguration);
		}
		//创建集群模式的连接工厂(yml只配置了普通模式,此处不判断不生效)
		if (getClusterConfiguration() != null) {
			return new JedisConnectionFactory(getClusterConfiguration(),
					clientConfiguration);
		}
		//创建普通模式的连接工厂
		return new JedisConnectionFactory(getStandaloneConfig(), clientConfiguration);
	}

我们先看第一行的方法:getJedisClientConfiguration() 获取jedis客户端的配置

private JedisClientConfiguration getJedisClientConfiguration() {
		//创建默认的JedisClientConfigurationBuilder对象,并设置useSsl,readTimeout,connectTimeout属性
		JedisClientConfigurationBuilder builder = applyProperties(JedisClientConfiguration.builder());
		//获取yml文件配置的pool,并配置JedisPoolConfig 设置到poolConfig属性中
		RedisProperties.Pool pool = this.properties.getJedis().getPool();
		if (pool != null) {
			applyPooling(pool, builder);
		}
		//处理url
		if (StringUtils.hasText(this.properties.getUrl())) {
			customizeConfigurationFromUrl(builder);
		}
		//执行定制器
		customize(builder);
		return builder.build();
	}

先看第一个JedisClientConfiguration.builder()方法,构建JedisClientConfigurationBuilder对象

class DefaultJedisClientConfigurationBuilder implements JedisClientConfigurationBuilder,
			JedisPoolingClientConfigurationBuilder, JedisSslClientConfigurationBuilder {

		private boolean useSsl;
		private @Nullable SSLSocketFactory sslSocketFactory;
		private @Nullable SSLParameters sslParameters;
		private @Nullable HostnameVerifier hostnameVerifier;
		private boolean usePooling;
		private GenericObjectPoolConfig poolConfig = new JedisPoolConfig();
		private @Nullable String clientName;
		private Duration readTimeout = Duration.ofMillis(Protocol.DEFAULT_TIMEOUT);
		private Duration connectTimeout = Duration.ofMillis(Protocol.DEFAULT_TIMEOUT);

		private DefaultJedisClientConfigurationBuilder() {}

上述yml配置中spring.timeout.nano =2 以及spring.timeout.seconds =2 ;在applyProperties中,设置了readTimeout 和connectTimeout属性,这个属性默认的就是Duration.ofMillis(Protocol.DEFAULT_TIMEOUT);后面的配置,大家就自己往下看;

我们再看创建JedisConnectionFactory的最后一步,在createJedisConnectionFactory()方法中,创建完JedisClientConfiguration,开始创建new JedisConnectionFactory(getStandaloneConfig(), clientConfiguration);

我们直接看JedisConnectionFactory中的获取连接的方法 Jedis jedis = fetchJedisConnector();

public RedisConnection getConnection() {

		if (isRedisClusterAware()) {
			return getClusterConnection();
		}

		Jedis jedis = fetchJedisConnector();
		String clientName = clientConfiguration.getClientName().orElse(null);
		JedisConnection connection = (getUsePool() ? new JedisConnection(jedis, pool, getDatabase(), clientName)
				: new JedisConnection(jedis, null, getDatabase(), clientName));
		connection.setConvertPipelineAndTxResults(convertPipelineAndTxResults);
		return postProcessConnection(connection);
	}

 

protected Jedis fetchJedisConnector() {
		try {

			if (getUsePool() && pool != null) {
				return pool.getResource();
			}

			Jedis jedis = createJedis();
			// force initialization (see Jedis issue #82)
			jedis.connect();

			potentiallySetClientName(jedis);
			return jedis;
		} catch (Exception ex) {
			throw new RedisConnectionFailureException("Cannot get Jedis connection", ex);
		}
	}

	private Jedis createJedis() {

		if (providedShardInfo) {
			return new Jedis(getShardInfo());
		}

		Jedis jedis = new Jedis(getHostName(), getPort(), getConnectTimeout(), getReadTimeout(), isUseSsl(),
				clientConfiguration.getSslSocketFactory().orElse(null), //
				clientConfiguration.getSslParameters().orElse(null), //
				clientConfiguration.getHostnameVerifier().orElse(null));

		Client client = jedis.getClient();

		getRedisPassword().map(String::new).ifPresent(client::setPassword);
		client.setDb(getDatabase());

		return jedis;
	}

后面的就可以不看了,拿到Jedis实例后,就可以做相关操作了;在创建实例的时候,传入的getConnectTimeout(),getReadTimeout()参数中,就是上述readTimeout属性Duration的toMillis()方法;获取的结果跟springboot1.x版本中的int类型的,是一致的

private int getConnectTimeout() {
		return Math.toIntExact(clientConfiguration.getConnectTimeout().toMillis());
	}

好了,redis在springboot2.0中,自动装配就是这样的,有兴趣的,还可以继续深入的看下去

 

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

葵花下的獾

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值