Redis连接池是否有必要?

安稳与你 提交于 2021-01-21 18:08:07

先上结论

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 on getConnection(). Use setValidateConnection(boolean) to change this behavior if necessary. Inject a Pool 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);
            });
        }

参考文档

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!