博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
SpringBoot自定义配置Redis连接需要注意的地方(结合JedisConnectionFactory源码)
阅读量:2225 次
发布时间:2019-05-09

本文共 6548 字,大约阅读时间需要 21 分钟。

背景:

今天测试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 causejava.lang.NullPointerException: null	at redis.clients.jedis.BinaryJedis.
(BinaryJedis.java:101) at redis.clients.jedis.Jedis.
(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只需要添加:

org.springframework.boot
spring-boot-starter-data-redis

2.0.0需要添加两个:

org.springframework.boot
spring-boot-starter-data-redis
redis.clients
jedis
2.9.0

原来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等。


转载地址:http://orlfb.baihongyu.com/

你可能感兴趣的文章
Oracle PL/SQL语言初级教程之操作和控制语言
查看>>
Oracle PL/SQL语言初级教程之过程和函数
查看>>
Oracle PL/SQL语言初级教程之表和视图
查看>>
Oracle PL/SQL语言初级教程之完整性约束
查看>>
PL/SQL学习笔记
查看>>
如何分析SQL语句
查看>>
结构化查询语言(SQL)原理
查看>>
SQL教程之嵌套SELECT语句
查看>>
日本語の記号の読み方
查看>>
计算机英语编程中一些单词
查看>>
JavaScript 经典例子
查看>>
判断数据的JS代码
查看>>
js按键事件说明
查看>>
AJAX 初次体验!推荐刚学看这个满好的!
查看>>
AJAX 设计制作 在公司弄的 非得要做出这个养的 真晕!
查看>>
Linux 查看文件大小
查看>>
Java并发编程:线程池的使用
查看>>
redis单机及其集群的搭建
查看>>
Java多线程学习
查看>>
检查Linux服务器性能
查看>>