先上结论
redis连接池主要作用体现在执行阻塞命令以及事务,通常情况下没有使用连接池的必要。
如何使用
先来看一下spring boot场景下如何使用redis。spring boot为各种sql,nosql提供了开箱即用的starter。这里关注如何配置集成redis,关于如何使用参考spring data redis文档。先来看一段spring boot document关于spring-boot-starter-data-redis的描述:
Spring Boot offers basic auto-configuration for the Lettuce and Jedis client libraries and the abstractions on top of them provided by Spring Data Redis.By default, it uses Lettuce.
By default, if
commons-pool2is on the classpath, you get a pooled connection factory.
spring boot默认使用lettuce作为客户端,连接池需要依赖commons-pool2。所以pom里声明如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.9.0</version>
</dependency>
翻一下LettuceConnectionConfiguration和RedisAutoConfiguration源码:
@Bean
@ConditionalOnMissingBean(RedisConnectionFactory.class)
LettuceConnectionFactory redisConnectionFactory(
ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers,
ClientResources clientResources) {
LettuceClientConfiguration clientConfig = getLettuceClientConfiguration(builderCustomizers, clientResources,
getProperties().getLettuce().getPool());
return createLettuceConnectionFactory(clientConfig);
}
@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的相关配置,注入了一个名字叫做redisTemplate和一个类型为StringRedisTemplate的bean。所以StringRedisTemplate是可以直接使用的,如果你需要定制redistemplate,自己声明一个bean,如果需要覆盖默认的redisTemplate,声明一个bean,名字一样就ok了。
所以如果配置文件里这样写,是不是意味着连接池里有20个连接可以用了?
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.lettuce.pool.max-idle=20
如何工作
要理解为什么要使用连接池,就必须先搞明白连接池有什么好处?工作原理是怎样的?毫无疑问,池化是为了复用资源,复用连接减少了每次都新创建连接的开销,最直观的就是避免了重复创建对象和tcp三次握手的过程。既然如此,每次都使用一个连接不就好了吗,第一次建立连接后不断开,一直保持这个连接,客户端一直持有连接对象,每次都复用这个连接,为什么会有最大连接数的配置?当然是为了并发了。
一般连接池都有最大连接数,空闲连接数,以及连接用尽后的策略等配置。随着并发请求的不断增大,连接会被一直创建直到达到最大连接数。如果并发再增加就看用尽策略了,可以继续新创建连接,也可以等待前面的连接放回池子中。当并发减少后,池子里会保持一个最小空闲数,多余的就被释放了。
这里要理解的是,连接并不是一个请求或者一个方法执行结束后才会放回池子,通常都是调用io操作完成后,也就是那句访问数据库的代码执行完成后就放回池子,所以同时有100个线程并发,并不意味着占用了100个连接,连接应当是远远小于线程数的,这个要看io操作的耗时。
根据以上理论,只需要配置好连接池,模拟高并发情景,在redis-cli 里执行client list,就应当能看到客户端建立了很多个连接。可是无论怎么模拟高并发,都是1个连接。是并发不够?尝试执行一个耗时的操作:
ExecutorService executor = Executors.newFixedThreadPool(100);
for (int i = 0; i < 100; i++) {
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " start,time=" + System.currentTimeMillis());
Set<String> keys = stringRedisTemplate.keys("*");
System.out.println(Thread.currentThread().getName() + " end,time=" + System.currentTimeMillis());
});
}
往redis丢上100万数据,然后执行keys操作,够耗时了吧,再次查看client list,仍旧是一个连接。上面的理论有问题?
是否必要
当发现配置文件不管用时,再次翻阅文档,Lettuce的官方文档发出了灵魂拷问:Is connection pooling necessary?
All Redis user operations are executed single-threaded. Using multiple connections does not impact the performance of an application in a positive way. 这句话描述的是redis服务器处理是单线程,客户端使用多连接并不会显著的提高性能。
单机redis性能能达到8万qps,因为redis性能太高了,那么连接池对于redis来讲是否是个没用的东西?远程访问查询一次redis究竟耗时多久?从客户端发送命令到远程redis服务器,redis执行查询,返回数据,我们就夸大一些,假设个1毫秒吧。使用多个连接的究极原因是并发,即这个连接正在被使用中还没有放回连接池,这时候有新的请求需要访问数据库,不能复用这个连接,所以需要一个新的连接。对于一个耗时1毫秒的操作,即使每次都复用1个连接也能支持1000qps了。如果需要增大连接池的数量,不如先考虑一下你的单实例程序能否支持1000qps。
The shared native connection is never closed by
LettuceConnection, therefore it is not validated by default ongetConnection(). UsesetValidateConnection(boolean)to change this behavior if necessary. Inject aPoolto pool dedicated connections. If shareNativeConnection is true, the pool will be used to select a connection for blocking and tx operations only, which should not share a connection. If native connection sharing is disabled, the selected connection will be used for all operations.
Lettuce默认使用一个 shared native connection,并且永远不会关闭,也就是一直复用这一个连接,连接池的配置仅是为了那些redis中那些阻塞的(比如 BLPOP)和事务的命令。所以,当使用Lettuce作为底层redis client时,redis配置文件里面那些个连接池的配置,仅当执行redis的阻塞命令以及事务操作时才会生效。执行如下代码,你会看到连接池满的效果:
ExecutorService executor = Executors.newFixedThreadPool(100);
for (int i = 0; i < 20; i++) {
executor.execute(() -> {
stringRedisTemplate.opsForList().leftPop("list", 10, TimeUnit.SECONDS);
});
}
参考文档
来源:oschina
链接:https://my.oschina.net/wecanweup/blog/4918348