SpringBoot自定义配置Redis连接需要注意的地方(结合JedisConnectionFactory源码)

背景:
今天测试redis自定义配置时出现了连接空指针的问题,并且同样代码在不同版本下表现不同,让我们来结合源码详细分析下问题所在。


一、问题起因

起初我们SpringBoot使用的是1.5.9版本,在自定义RedisTemplate各种参数配置时出现了问题:

	@Bean(name = "foreRedisTemplate")
    public RedisTemplate getForeRedisTemplate(){

        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(maxIdle);
        jedisPoolConfig.setMinIdle(minIdle);
        jedisPoolConfig.setMaxWaitMillis(maxWait);

        JedisConnectionFactory connectionFactory = new JedisConnectionFactory();
        connectionFactory.setPoolConfig(jedisPoolConfig);
        connectionFactory.setDatabase(foreDatabase);
        connectionFactory.setHostName(host);
        connectionFactory.setPassword(password);
        connectionFactory.setPort(port);
        connectionFactory.setTimeout(timeout);

        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate(connectionFactory);
        return stringRedisTemplate;
    }

运行测试后报错如下:
Cannot get Jedis connection; nested exception is java.lang.NullPointerException

[2019-02-17 01:10:17.943] ERROR [http-nio-8080-exec-1] DirectJDKLog.java:181 - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; 
nested exception is org.springframework.data.redis.RedisConnectionFailureException: 
Cannot get Jedis connection; nested exception is java.lang.NullPointerException] with root cause
java.lang.NullPointerException: null
	at redis.clients.jedis.BinaryJedis.<init>(BinaryJedis.java:101)
	at redis.clients.jedis.Jedis.<init>(Jedis.java:78)
	at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:197)
	at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.getConnection(JedisConnectionFactory.java:348)
	at org.springframework.data.redis.core.RedisConnectionUtils.doGetConnection(RedisConnectionUtils.java:129)
	at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:92)
	at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:79)
	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:194)
	at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:169)
	at org.springframework.data.redis.core.AbstractOperations.execute(AbstractOperations.java:91)
	at org.springframework.data.redis.core.DefaultValueOperations.set(DefaultValueOperations.java:169)

可能熟悉上面配置代码的同学发现了问题所在,貌似缺少了一步connectionFactory.afterPropertiesSet()

但是我保持代码不变,将springboot版本切换到2.0.0,居然又没有报错,这是为啥?


题外话:
我在切换版本时发现一个有意思的改动:
在这里插入图片描述
在这里插入图片描述
1.5.9版本的 spring-boot-starter-data-redis maven依赖是包含jedis的,但到了2.0.0版本,却不包含了,需要我们手动添加jedis依赖:

1.5.9只需要添加:

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

2.0.0需要添加两个:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
	<version>2.9.0</version>
</dependency>

原来SpringBoot2.X默认采用lettuce,而1.5默认采用的是jdeis;Lettuce和Jedis都是连接Redis Server的客户端程序,Jedis在实现上是直连redis server,多线程环境下非线程安全,除非使用连接池,为每个Jedis实例增加物理连接。Lettuce基于Netty的实例连接,可以再多个线程间并发访问,且线程安全,满足多线程环境下的并发访问,同时它是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。


二、问题分析

我们通过报错信息可以大概知道,JedisConnectionFactorygetConnectionfetchJedisConnector方法产生了空指针错误,下面我们来分析JedisConnectionFactory源码,定位问题源头。

先看1.5.9版本的源码:
在这里插入图片描述
进入fetchJedisConnector方法:

protected Jedis fetchJedisConnector() {
	try {

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

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

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

该方法先判断是否使用并且存在pool,如果不满足条件则new一个Jedis对象,并传入 getShardInfo() 作为构造参数,继续看该方法以及Jedis构造函数:

// 获取JedisConnectionFactory类的shardInfo属性
public JedisShardInfo getShardInfo() {
	return shardInfo;
}
public Jedis(JedisShardInfo shardInfo) {
	super(shardInfo);
}
// BinaryJedis类为Jedis父类
public BinaryJedis(final JedisShardInfo shardInfo) {
	client = new Client(shardInfo.getHost(), shardInfo.getPort(), shardInfo.getSsl(),
          shardInfo.getSslSocketFactory(), shardInfo.getSslParameters(),
          shardInfo.getHostnameVerifier());
	client.setConnectionTimeout(shardInfo.getConnectionTimeout());
	client.setSoTimeout(shardInfo.getSoTimeout());
	client.setPassword(shardInfo.getPassword());
	client.setDb(shardInfo.getDb());
}

可以看到BinaryJedis类中使用了大量shardInfo对象的方法,那么问题来了,shardInfo会为空吗?

我们观察整个JedisConnectionFactory类,发现只有一个地方给shardInfo对象初始化,那就是 afterPropertiesSet 方法!

	public void afterPropertiesSet() {
		// 此处真正给shardInfo赋值
		if (shardInfo == null) {
			shardInfo = new JedisShardInfo(hostName, port);

			if (StringUtils.hasLength(password)) {
				shardInfo.setPassword(password);
			}

			if (timeout > 0) {
				setTimeoutOn(shardInfo, timeout);
			}
		}
		
		if (usePool && clusterConfig == null) {
			this.pool = createPool();
		}

		if (clusterConfig != null) {
			this.cluster = createCluster();
		}
	}

可以看到,该方法不仅保证了shardInfo 不为空,还创建了pool对象(非cluster模式下)(注意该类的usePool 属性默认值就是true),后面的jedis连接都是从pool里获取资源了!

所以报错原因就是没有调用afterPropertiesSet方法,导致shardInfo对象为空,之后调用shardInfo.getHost()等就报错空指针了!

那为什么2.0.0版本不加afterPropertiesSet方法没事呢?

我们再看看2.0.0版本的源码:

protected Jedis fetchJedisConnector() {
	try {

		if (getUsePool() && pool != null) {
			return pool.getResource();
		}
		// 此处注意了,和1.5.9版本不同
		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);
	}
}

可以看到,1.5.9版本是new Jedis(getShardInfo())来创建jedis对象,但这里是调用createJedis()方法来创建的,看看这个方法:

private Jedis createJedis() {
	// 该属性默认为false
	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对象先做了一步判断providedShardInfo,相当于非空校验,不像之前那样直接就干。。下面就是中规中矩的new Jedis()创建对象了;

看到这里,相信大家已经明白了开篇的问题究竟为何如此了;那么问题来了,我们到底需要加上afterPropertiesSet方法吗?
答案是肯定的,调用该方法才可以使我们配置的各种参数生效,比如pool、cluster等。


发布了73 篇原创文章 · 获赞 373 · 访问量 42万+

猜你喜欢

转载自blog.csdn.net/Abysscarry/article/details/87487891
今日推荐