先上结论
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-pool2
is 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 aPool
to 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