配置的那点玄学

前言

我是[提前退休的java猿],一名7年java开发经验的开发组长,分享工作中的各种问题!

今天写这篇文章的背景就是在排查问题的时候,debug了 连接池和驱动的源码。然后就发现了几个坑儿。

😭这个坑儿让你的配置不生效,生效了但是只生效了一半,生效了但是为什么时间给我扩大了1000倍呢

比如:我现在需要调整数据库链接的两个参数一个是链接超时时间,一个是socket读取数据时间。

正常来说有两种配置方式:

    1. 跟到url后面, url: jdbc:kingbase8......&socket-timeout=300000&connect-timeout=300000
    1. 设置到连接池提供的参数上:
java 复制代码
socket-timeout: 300000
connect-timeout: 300000

❔今天就通过源码的方式,看看这两种情况的配置究竟都存在什么问题

😱debug 环境:驱动是kingbase+连接池是durid+集群模式

国产驱动的配置玄学

connect-timeout、socket-timeout 读取设置过程

首先大家对这个参数的单位应该没有什么疑问吧,是毫秒对吧。那我们就开始分析问题吧

1. 创建连接池:读取线程池参数

创建链接池,读取连接池的配置并把配置set到连接池上

java 复制代码
public DataSource createDataSource(DataSourceProperty dataSourceProperty) {
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setUsername(dataSourceProperty.getUsername());
    dataSource.setPassword(dataSourceProperty.getPassword());
    dataSource.setUrl(dataSourceProperty.getUrl());
    dataSource.setName(dataSourceProperty.getPoolName());
    String driverClassName = dataSourceProperty.getDriverClassName();
    if (DsStrUtils.hasText(driverClassName)) {
        dataSource.setDriverClassName(driverClassName);
    }
    DruidConfig config = dataSourceProperty.getDruid();
    Properties properties = DruidConfigUtil.mergeConfig(gConfig, config);

    List<Filter> proxyFilters = this.initFilters(dataSourceProperty, properties.getProperty("druid.filters"));
    dataSource.setProxyFilters(proxyFilters);
    try {
        configMethod.invoke(dataSource, properties);
    } catch (Exception ignore) {

    }
    //连接参数单独设置
    dataSource.setConnectProperties(config.getConnectionProperties());
    //设置druid内置properties不支持的的参数 : connectTimeout 和 socketTimeout 就包括在这个里面
    for (String param : PARAMS) {
        // 单个数据源的配置 会覆盖 全局的连接池配置
        DruidConfigUtil.setValue(dataSource, param, gConfig, config);
    }

    if (Boolean.FALSE.equals(dataSourceProperty.getLazy())) {
        try {
            dataSource.init();
        } catch (SQLException e) {
            throw new ErrorCreateDataSourceException("druid create error", e);
        }
    }
    return dataSource;
}

2. 连接池的初始化,设置默认配置

初始化过程,为0则使用线程池默认配置10_000 ms,伪代码如下DruidDataSource.init():

java 复制代码
public void init() throws SQLException {
        .............
        // set 默认配置
        if (connectTimeout == 0) {
            connectTimeout = DEFAULT_TIME_CONNECT_TIMEOUT_MILLIS;
        }
        if (socketTimeout == 0) {
            socketTimeout = DEFAULT_TIME_SOCKET_TIMEOUT_MILLIS;
        }
        ..........
        while(...){
            // 创建链接
            PhysicalConnectionInfo pyConnectInfo = createPhysicalConnection();
        }

3.调用驱动创建链接:传递连接池的配置

判断驱动类型,根据具体的驱动进行参数类型的转换,传递 connectTimeout、socketTimeout 参数给驱动创建连接,创建连接之后,调用setNetworkTimeout

驱动源码:DruidAbstractDataSource.createPhysicalConnection()

java 复制代码
public PhysicalConnectionInfo createPhysicalConnection() throws SQLException {
    String url = this.getUrl();
    Properties connectProperties = getConnectProperties();
    。。。。。。。。。。。。。。。。。。。。
    //1. 此时的connectTimeout、connectTimeout 是连接池的配置(默认)
    if (connectTimeout > 0) {
        if (isMySql) {
            if (connectTimeoutStr == null) {
                connectTimeoutStr = Integer.toString(connectTimeout);
            }
            physicalConnectProperties.put("connectTimeout", connectTimeoutStr);
        } else if (isOracle) {
            if (connectTimeoutStr == null) {
                connectTimeoutStr = Integer.toString(connectTimeout);
            }
            physicalConnectProperties.put("oracle.net.CONNECT_TIMEOUT", connectTimeoutStr);
        } else if (driver != null && POSTGRESQL_DRIVER.equals(driver.getClass().getName())) {
            // see https://github.com/pgjdbc/pgjdbc/blob/2b90ad04696324d107b65b085df4b1db8f6c162d/README.md
            physicalConnectProperties.put("loginTimeout", Long.toString(TimeUnit.MILLISECONDS.toSeconds(connectTimeout)));
            physicalConnectProperties.put("connectTimeout", Long.toString(TimeUnit.MILLISECONDS.toSeconds(connectTimeout)));
            if (socketTimeout > 0) {
                physicalConnectProperties.put("socketTimeout", Long.toString(TimeUnit.MILLISECONDS.toSeconds(socketTimeout)));
            }
        } else if (DbType.sqlserver.name().equals(dbTypeName)) {
            // see https://learn.microsoft.com/en-us/sql/connect/jdbc/setting-the-connection-properties?view=sql-server-ver16
            physicalConnectProperties.put("loginTimeout", Long.toString(TimeUnit.MILLISECONDS.toSeconds(connectTimeout)));
            if (socketTimeout > 0) {
                // As SQLServer-jdbc-driver 6.1.2 can use this, see https://github.com/microsoft/mssql-jdbc/wiki/SocketTimeout
                physicalConnectProperties.put("socketTimeout", Integer.toString(socketTimeout));
            }
        }
    }
    Connection conn = null;
    ................................
    try {
        // 上面的处理情况并没有对 kingbase 驱动做处理,所以 physicalConnectProperties 没有 connect/socket-timeout 两个值
        conn = createPhysicalConnection(url, physicalConnectProperties);

        initPhysicalConnection(conn, variables, globalVariables);
        initedNanos = System.nanoTime();
        ......
        // 这个地方也是重点,设置netWorkTimeout,socketTimeout 为连接池配置(默认)的参数
        // As SQLServer-jdbc-driver 6.1.7 can use this, see https://github.com/microsoft/mssql-jdbc/wiki/SocketTimeout
        conn.setNetworkTimeout(netTimeoutExecutor, socketTimeout); // here is milliseconds defined by JDBC
        ........
    } catch (SQLException ex) {
       ..............
    } finally {
       ........
    }
    return new PhysicalConnectionInfo(conn, connectStartNanos, connectedNanos, initedNanos, validatedNanos, variables, globalVariables);
}
3.1创建连接

创建连接: 省略中间的API,从上面我们得出了结论,就是durid在创建连接的时候,并没有把conn/socket-timeout参数传进来,下面infoProps这个参数就是从url解析出来的

  • connectTimeout 从url读取(默认10)* 1000 ,这个参数的单位居然是秒😱(我们在url中设置了connectTimeout都挡在ms来使用的,那就嘿嘿了)
  • socketTimeOut 从url读取(默认0),集群模式下面单位应该是按照ms来使用的,单点情况下面*1000应该按照s来设置的 驱动创建conn底层设置socket相关的代码如下com.kingbase8.core.v3.ConnectionFactoryImpl.tryConnect()
java 复制代码
private KBStream tryConnect(String user, String database, Properties infoProps, SocketFactory socketFactory, HostSpec _hostSpec, SslMode sslMode, int _version, Object cCMV2) throws SQLException, IOException {
    TraceLogger.logLineInfo(Level.ALL, "lineInfo");
    // ❗从infoProps中获取,没有就使用默认 10,可以看到这个地方的 connect-timeout 的单位是s😱
    int connectTimeoutT = KBProperty.CONNECT_TIMEOUT.getInt(infoProps) * 1000;
    // 会创建socket会话,connectTimeoutT 就是创建socket连接的超时时间
    KBStream newStreamT = new KBStream(socketFactory, _hostSpec, connectTimeoutT, KBProperty.USEDISPATCH.getBoolean(infoProps) && infoProps.getProperty("isMonitor") == null, _version, cCMV2);
    TraceLogger.logLineInfo(Level.ALL, "lineInfo");
    // 从infoproprs 中获取,没有则使用默认值0 这个地方也没有 *1000 呢,(被当成ms来使用了吗)
    int socketTimeoutT = KBProperty.SOCKET_TIMEOUT.getInt(infoProps);
    if (KBProperty.USEDISPATCH.getBoolean(infoProps) && infoProps.getProperty("isMonitor") == null) {
        TraceLogger.logLineInfo(Level.ALL, "lineInfo");
        if (socketTimeoutT < 0) {
            TraceLogger.logLineInfo(Level.ALL, "lineInfo");
            socketTimeoutT = 0;
        }

        newStreamT.getSocket().setSoTimeout(1000);
        newStreamT.setSocketTimeout(socketTimeoutT);
        KBLOGGER.log(Level.INFO, "Dispatch : socketTimeout is " + socketTimeoutT * 1000, new Object[0]);
    } else {
        TraceLogger.logLineInfo(Level.ALL, "lineInfo");
        if (socketTimeoutT > 0) {
            TraceLogger.logLineInfo(Level.ALL, "lineInfo");
            // 非集群模式下面 socketTimeout 又当成 秒来使用
            newStreamT.getSocket().setSoTimeout(socketTimeoutT * 1000);
            KBLOGGER.log(Level.INFO, "Single or Monitor : socketTimeout is " + socketTimeoutT * 1000, new Object[0]);
        }
    }
     .................................
    return newStreamT;
}
3.2 调用setNetworkTimeout 再次设置超时时间

创建conn之后(DruidAbstractDataSource.createPhysicalConnection()),会调用conn的setNetworkTimeout 方法对超时时间重新设置,参数是从连接池传入的 ,我们看看Kingbase实现的具体逻辑com.kingbase8.core.KBStream.setNetworkTimeout()

java 复制代码
public void setNetworkTimeout(int millisecs) throws IOException {
    TraceLogger.logLineInfo(Level.ALL, "lineInfo");
    //集群模式,再次写死 soTimeout 为1s
    if (this.isUseDispatch()) {
        TraceLogger.logLineInfo(Level.ALL, "lineInfo");
        this.connectionSocket.setSoTimeout(1000);
        //这个参数时间上是用来计算 重试次数的
        this.setSocketTimeout(millisecs % 1000 == 0 ? millisecs / 1000 : millisecs / 1000 + 1);
        KBLOGGER.log(Level.INFO, "Dispatch : socketTimeout is " + millisecs, new Object[0]);
    } else {
        TraceLogger.logLineInfo(Level.ALL, "lineInfo");
        this.connectionSocket.setSoTimeout(millisecs);
        KBLOGGER.log(Level.INFO, "Single or Monitor : socketTimeout is " + millisecs, new Object[0]);
    }

}
  • 再次写死Socket timeout 的时间为1000ms,就是1s
  • 把连接池中的socketTimeout,设置this.socketTimeout的时候除以了1000,得到重试次数

看一下socketTimeout在哪用到了,VisibleBufferedInputStream.readMore

java 复制代码
private boolean readMore(int wanted) throws IOException {
    .................................
    TraceLogger.logLineInfo(Level.ALL, "lineInfo");
    int readT = true;
    int i = 0;
    int j = 0;

    int readT;
    while(true) {
        try {
        //----------------------------------------读取响应数据-----------------------------
            readT = this.wrappedInputStream.read(this._buffer, this.endIndex, canFit);
            break;
        } catch (SocketTimeoutException var10) {
            TraceLogger.logLineInfo(Level.ALL, "lineInfo");
            if (this.useDispatch) {
                if ((this.pCMV2.master_online_ip.equals(this._host) || this.pCMV2.slave_online_ip.contains(this._host + ",")) && (Integer)this.pCMV2._connVersion.get(this._host) == this._version) {
                    label78: {
                        // -------------------重试 socketTimeout 次数,------------
                        if (this.socketTimeout != 0) {
                            ++i;
                            if (i >= this.socketTimeout) {
                                break label78;
                            }
                        }

                        ++j;
                        if (j % 5 == 0) {
                            KBLOGGER.log(Level.INFO, "Online _host {0} has been waiting for {1} times, socketTimeout is {2}", new Object[]{this._host, j, this.socketTimeout});
                        }
                        continue;
                    }  
                }
            ............................
            }
            throw var10;
        }
    }
 ..................
}

总结

通过源码分析了connect-timeoutsocket-timeout 在集群模式下设置值的过程。我们可以得出以下结论

✔集群模式要配置这两个参数:

  • connect-timeout: 跟到url后面并且单位为s:url: jdbc:kingbase8://172.xxx&connectTimeOut=30,配置到线程池无效
  • socket-timeout:配置到连接池中,单位为ms:spring.datasource.dynamic.druid.connect-timeout= 30*1000 单位为ms,配置到url后无效

😭所以这就很坑啊,这两个参数都是控制socket超时的参数,正常逻辑单位或者是设置的地方应该保一致直才对,结果通过我们的源码去分析了,各有各的单位,各有各的配置方式。

这种情况,就会让很多程序员感到十分的懵,可能改了参数,发现没有生效。设置了时间发现和自己配置的不一样,所以一切的玄学背后皆有源头🤞


本篇文章,分析了driud+kingbase的环境,加载配置的流程,发现了同样的参数在 集群和单点环境加载的方式还不一样,连时间单位都不统一!其他驱动和连接池是否存在这个问题,欢迎大家留言评论

相关推荐
我来整一篇4 分钟前
[mysql] 深分页优化
java·数据库·mysql
Sadsvit7 分钟前
Linux 服务器性能监控、分析与优化全指南
java·linux·服务器
hqxstudying10 分钟前
Java开发时出现的问题---语言特性与基础机制陷阱
java·jvm·python
kinlon.liu22 分钟前
内网穿透 FRP 配置指南
后端·frp·内网穿透
kfyty72531 分钟前
loveqq-mvc 再进化,又一款分布式网关框架可用
java·后端
CodeCraft Studio37 分钟前
使用 Aspose.OCR 将图像文本转换为可编辑文本
java·人工智能·python·ocr·.net·aspose·ocr工具
Dcr_stephen39 分钟前
Spring 事务中的 beforeCommit 是业务救星还是地雷?
后端
raoxiaoya1 小时前
Golang中的`io.Copy()`使用场景
开发语言·后端·golang
二闹1 小时前
高效开发秘籍:CRUD增强实战
后端·设计模式·性能优化
我爱娃哈哈1 小时前
Eureka vs Consul,服务注册发现到底选哪个?性能对比深度解析!
后端