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


  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
SpringBoot项目中配置Redis的步骤如下: 1. 首先,在pom.xml文件中添加Redis的依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.7.3</version> </dependency> ``` 这将引入SpringBootRedis的支持。 2. 然后,在application.yml或application.properties配置文件中进行相应的Redis配置。你可以配置Redis的主机地址、端口号、密码等信息。例如: ```yaml spring: redis: host: localhost port: 6379 password: your_password # 其他配置项 ``` 3. 接下来,你需要创建一个配置类来配置Redis连接工厂。在这个配置类上使用`@Configuration`注解,并使用`@EnableCaching`注解开启缓存支持。你还可以通过`@Bean`注解创建一个`RedisTemplate`实例,并配置序列化器等参数。例如: ```java @Configuration @EnableCaching public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(connectionFactory); // 配置序列化器等参数 // ... return redisTemplate; } } ``` 4. 最后,在启动类上加上`@SpringBootApplication`注解,并在类上面加上`@EnableCaching`注解开启缓存支持。这样就完成了Redis配置。例如: ```java @SpringBootApplication @EnableCaching public class YourApplication { public static void main(String[] args) { SpringApplication.run(YourApplication.class, args); } } ``` 这样,你就成功地在SpringBoot项目中配置Redis。你可以根据实际需求,通过Redis来缓存频繁使用的数据,提高查询速度。在实际项目中,例如博客项目,可以将文章的浏览量存储到Redis中,提高查询效率。同时,在秒杀场景下也可以使用Redis来处理高并发请求。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值