目录
背景
最近在排查问题时,发现有些流会超时(在最终submit的地方配置了.timeout()),但是这些流本身运行逻辑很简单。通过在不同的位置增加.log()进行记录,最终发现,在数据库操作部分超时了。由于本身业务SQL并不是慢查询,是一个非常简单的SQL,所以在R2DBC的配置上,增加了connectTimeout属性,最终确定是连接数据库超时导致的。报错如下:
17:11:59.665 [ERROR] [parallel-5] ReactorNettyClient |-> Exchange cancelled while exchange is active. This is likely a bug leading to unpredictable outcome.
到这里,突然想起来,R2DBC的配置没有使用连接池!
关于连接池
我们来看看Druid下的一些熟悉的连接池配置(具体可见官方文档)
- maxActive
- maxIdle
- minIdle
- validationQuery
对!这些配置就是在Druid的DruidDataSource中的最基础配置,用于配置连接池大小,以及如何进行探活。
而在r2dbc-mysql官方GitHub的WiKi中,并没有对应的Demo,以至于笔者最开始在使用R2DBC的时候,没有使用连接池技术。
后来尝试去寻找一些资料,但是无论在google还是百度上,都没有找到R2dbc的pool配置。靠人不如靠自己,还是自己动手吧。
环境
- SpringBoot 2.4
- r2dbc-bom:Arabba-SR9
- r2dbc-mysql
- spring-boot-starter-data-r2dbc
R2DBC连接池配置
结论
根据惯例,先给出结论,给到希望直接打到DEMO的小伙伴
我们在WebFlux中使用R2DBC的时候,需要构建出ConnectionFactory,它是SQL访问和构建其它工具类(如R2dbcEntityTemplate)的基础。
引用部分
仅仅构建连接池,并不是全部引用都能用到,我就是懒,都复制过来了
import dev.miku.r2dbc.mysql.MySqlConnectionConfiguration;
import dev.miku.r2dbc.mysql.MySqlConnectionFactory;
import io.r2dbc.pool.ConnectionPool;
import io.r2dbc.pool.ConnectionPoolConfiguration;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.data.r2dbc.core.DefaultReactiveDataAccessStrategy;
import org.springframework.data.r2dbc.core.R2dbcEntityOperations;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.r2dbc.dialect.MySqlDialect;
import java.time.Duration;
常量部分
这部分的配置根据自己的需求进行调整
private final static Duration DEFAULT_TCP_TIMEOUT = Duration.ofSeconds(5);
private final static int DEFAULT_INITIAL_SIZE = 5;
private final static int DEFAULT_MAX_SIZE = 20;
private final static Duration DEFAULT_MAX_ACQUIRE_TIME = Duration.ofSeconds(10);
private final static Duration DEFAULT_MAX_CREATE_CONNECTION_TIME = Duration.ofSeconds(5);
private final static Duration DEFAULT_BACKGROUND_EVICTION_INTERVAL = Duration.ofMinutes(20);
private final static int DEFAULT_ACQUIRE_RETRY = 2;
ConnectionFactory
/**
* 创建MySQL的ConnectionFactory的公共方法
*
* @param mySqlConfig mySQL连接配置
* @param dbName 用于日志记录的Bean的描述,通常可以写成DB信息
* @return 返回MySQL的ConnectionFactory
*/
protected ConnectionFactory createMySqlConnectionFactory(DatabaseConnectionProperties.MySql mySqlConfig, String dbName) {
//记录日志
log.info("Begin to create connection factory for {}.Get the Configuration :{}", dbName, mySqlConfig.toString());
//配置基础连接参数
MySqlConnectionConfiguration configuration = MySqlConnectionConfiguration.builder()
.host(mySqlConfig.getHost())
.port(mySqlConfig.getPort())
.username(mySqlConfig.getUsername())
.password(mySqlConfig.getPassword())
.database(mySqlConfig.getDatabase())
.connectTimeout(DEFAULT_TCP_TIMEOUT)
.tcpNoDelay(true)
.tcpKeepAlive(true)
.build();
log.info("The MySQL connection configuration for {} has been created:{}", dbName, configuration);
//构建MySQL的ConnectionFactory
ConnectionFactory factory = MySqlConnectionFactory.from(configuration);
//【重点】建立连接池
ConnectionPoolConfiguration poolConfiguration = ConnectionPoolConfiguration.builder(factory)
//启动时默认连接池数量
.initialSize(DEFAULT_INITIAL_SIZE)
//最长空闲时间
.maxIdleTime(DEFAULT_BACKGROUND_EVICTION_INTERVAL)
//连接池最大大小
.maxSize(DEFAULT_MAX_SIZE)
//获取连接超时时间
.maxAcquireTime(DEFAULT_MAX_ACQUIRE_TIME)
//创建连接超时时间
.maxCreateConnectionTime(DEFAULT_MAX_CREATE_CONNECTION_TIME)
//获取连接重试次数
.acquireRetry(DEFAULT_ACQUIRE_RETRY)
//连接池名称
.name("r2dbc_pool_" + dbName)
//用于测试连接是否可用的语句
.validationQuery("SELECT 1")
//是否注册JMX
.registerJmx(true)
.build();
ConnectionPool connectionPool = new ConnectionPool(poolConfiguration);
log.info("The factory creative option for {} has been done.", dbName);
return connectionPool;
}
官方文献
r2dbc-mysql
其实r2dbc-mysql的官方文档中并不是完全没有提及Pool配置,之前也忽略了这一点,在文档中有个Pooling节点,提到了连接池:
是的,就2个单词。
r2dbc-pool
R2DBC使用的连接池实现即是r2dbc-pool,在官方文档中给出了我们熟悉的配置的说明
但可惜的是没有给出MySQL的相关DEMO。但是可以根据r2dbc-mysql和r2dbc-pool的文档综合起来看。
r2dbc-pool的Demo
完全copy官方文档
// Creates a ConnectionFactory for the specified DRIVER
ConnectionFactory connectionFactory=ConnectionFactories.get(ConnectionFactoryOptions.builder()
.option(DRIVER,"postgresql")
.option(HOST,"…")
.option(PORT,"…")
.option(USER,"…")
.option(PASSWORD,"…")
.option(DATABASE,"…")
.build());
// Create a ConnectionPool for connectionFactory
ConnectionPoolConfiguration configuration=ConnectionPoolConfiguration.builder(connectionFactory)
.maxIdleTime(Duration.ofMillis(1000))
.maxSize(20)
.build();
ConnectionPool pool=new ConnectionPool(configuration);
Mono<Connection> connectionMono = pool.create();
// later
Connection connection = …;
Mono<Void> release = connection.close(); // released the connection back to the pool
// application shutdown
pool.dispose();
注意到了吗?这里ConnectionPoolConfiguration的builder的入参是一个ConnectionFactory,而r2dbc-mysql的Demo中使用到的MySqlConnectionFactory最终构建出来的就是一个ConnectionFactory
到此,2个组件的相互关系就已经连上了。
相关配置
由于配置说明都是英文的, 我大概挑选了几个关键的说一下自己的理解
配置项 | 说明 |
---|---|
initialSize | 启动时连接池大小,默认10 |
maxIdleTime | 最长空闲时间。如果配置为负数,则连接不会因为空闲等待而被释放。默认30分钟。注意:这个配置和backgroundEvictionInterval重复用途,这个后面说明 |
maxSize | 连接池最大大小,默认10 |
maxAcquireTime | 获取连接的最长时间。也就是说,sql请求过来后,会向连接池请求一个连接,当连接全忙或者需要发起新的连接的时候,请求会处于等待状态。这个值配置最长等待多久,默认没有等待超时时间,如果获取不到会一直等下去。建议配置一下,避免流被卡住 |
maxCreateConnectionTime | 创建连接的超时时间,默认不会超时 |
acquireRetry | 请求连接的重试次数,默认为1 |
name | 连接池名称 |
validationQuery | 探活SQL |
registerJmx | 是否注册JMX |
关于backgroundEvictionInterval和maxIdleTime
这两个配置根据文档说明,作用有重叠的地方。所以为了探究他俩的左右,我专门去找了一下源码。先说一下结论:实际效果都作用在同一个地方,配置任意一个即可,无需2个都配置。
首先可以看到源码中的用法,在ConnectionPool中的216行位置
Duration backgroundEvictionInterval = configuration.getBackgroundEvictionInterval();
if (!backgroundEvictionInterval.isZero()) {
if (!backgroundEvictionInterval.isNegative()) {
builder.evictInBackground(backgroundEvictionInterval);
} else if (!configuration.getMaxIdleTime().isNegative()) {
builder.evictInBackground(configuration.getMaxIdleTime());
}
}
而在测试用例中,也能看到其中的区别。详见测试用例