【历代文学】Druid连接池实战:搞懂6个参数,就能用好它

举报
程风破浪 发表于 2024/11/28 11:22:57 2024/11/28
【摘要】 你是否遇到数据库因故障重启后,druid却无法自动恢复? 实际项目中,你是否经常会遇到以下问题: 如下日志所示,数据库宕机,重启恢复后,druid连接池却依然死翘翘,无法自动恢复,如下错误日志所示:

 

本文针对历代文学网点连接可进入)的Druid数据源调优实战总结分享给大家!

编辑

历代文学网数据库数据量高达1TGB,收录来自古今中外 200多个朝代和国家的作者超 15万人,诗、词、曲、赋、文言文等作品数超 114万个,成语超 5万个,名句超 12万条,名言超 130万条,著作超 2万部。


历代文学项目稳定上线前,曾多次经历数据库宕机重启后,Druid连接池无法自动恢复,除非手动重启应用程序,才能让Druid连接池自动恢复正常!


1. Druid实战场景简介

Druid是Java语言中最好的数据库连接池。Druid能够提供强大的监控和扩展功能。

Druid是一个开源项目,源码托管在github上,源代码仓库地址是:
 https://github.com/alibaba/druid

1.1 Druid数据源介绍

阿里巴巴的Druid是一个JDBC组件,它包含三部分:DruidDriver代理、DruidDataSource数据库连接池和SQLParser。Druid是阿里巴巴的开源项目,该项目主要是为了监控数据库连接池的性能指标,提供可视化的操作界面。

Druid连接池的优点:

  1. 可以监控数据库池的状态,包括池的状态及每个活动连接的详细状态。

  2. 可以提供SQL监控功能,可以监控SQL的执行时间、执行次数、执行频率等。

  3. 可以提供数据库密码加密功能,提高系统安全性。

  4. 支持数据库分表分库,提供异常连接处理机制,提高系统稳定性。

1.2 你是否遇到数据库因故障重启后,druid却无法自动恢复?

实际项目中,你是否经常会遇到以下问题:

如下日志所示,数据库宕机,重启恢复后,druid连接池却依然死翘翘,无法自动恢复,如下错误日志所示:

[2024-08-26 08:42:22,997][                                 c.a.d.p.DruidDataSource][ERROR][onPool-Create-2020751256][                                 >     ] create connection SQLException, url: jdbc:postgresql://172.16.10.80:5432/cloud-platform?useUnicode=true&tcpKeepAlive=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&reWriteBatchedInserts=true, errorCode 0, state 08001
org.postgresql.util.PSQLException: Connection to 172.16.10.80:5432 refused. Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connections.
	at org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(ConnectionFactoryImpl.java:346) ~[postgresql-42.7.3.jar!/:42.7.3]
	at org.postgresql.core.ConnectionFactory.openConnection(ConnectionFactory.java:54) ~[postgresql-42.7.3.jar!/:42.7.3]
	at org.postgresql.jdbc.PgConnection.<init>(PgConnection.java:273) ~[postgresql-42.7.3.jar!/:42.7.3]
	at org.postgresql.Driver.makeConnection(Driver.java:446) ~[postgresql-42.7.3.jar!/:42.7.3]
	at org.postgresql.Driver.connect(Driver.java:298) ~[postgresql-42.7.3.jar!/:42.7.3]
	at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:132) ~[druid-1.2.23.jar!/:?]
	at com.alibaba.druid.filter.FilterAdapter.connection_connect(FilterAdapter.java:764) ~[druid-1.2.23.jar!/:?]
	at com.alibaba.druid.filter.FilterEventAdapter.connection_connect(FilterEventAdapter.java:33) ~[druid-1.2.23.jar!/:?]
	at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:126) ~[druid-1.2.23.jar!/:?]
	at com.alibaba.druid.filter.stat.StatFilter.connection_connect(StatFilter.java:244) ~[druid-1.2.23.jar!/:?]
	at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:126) ~[druid-1.2.23.jar!/:?]
	at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:1687) ~[druid-1.2.23.jar!/:?]
	at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:1803) ~[druid-1.2.23.jar!/:?]
	at com.alibaba.druid.pool.DruidDataSource$CreateConnectionThread.run(DruidDataSource.java:2914) [druid-1.2.23.jar!/:?]
Caused by: java.net.ConnectException: Connection refused
	at java.base/sun.nio.ch.Net.pollConnect(Native Method) ~[?:?]
	at java.base/sun.nio.ch.Net.pollConnectNow(Net.java:682) ~[?:?]
	at java.base/sun.nio.ch.NioSocketImpl.timedFinishConnect(NioSocketImpl.java:549) ~[?:?]
	at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:592) ~[?:?]
	at java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:327) ~[?:?]
	at java.base/java.net.Socket.connect(Socket.java:752) ~[?:?]
	at org.postgresql.core.PGStream.createSocket(PGStream.java:243) ~[postgresql-42.7.3.jar!/:42.7.3]
	at org.postgresql.core.PGStream.<init>(PGStream.java:98) ~[postgresql-42.7.3.jar!/:42.7.3]
	at org.postgresql.core.v3.ConnectionFactoryImpl.tryConnect(ConnectionFactoryImpl.java:136) ~[postgresql-42.7.3.jar!/:42.7.3]
	at org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(ConnectionFactoryImpl.java:262) ~[postgresql-42.7.3.jar!/:42.7.3]

数据库宕机,再恢复数据库启动后,应用服务器的druid连接池,因为没有重启服务器,导致迟迟不能恢复正常?这里一定要重启应用服务器才能让druid恢复正常吗?难道真的是Druid自己存在的bug,导致数据库宕机恢复后,自己却不能恢复么?还是我们没有学会使用Druid数据源的核心配置导致的?

答案显然不是Druid自身的bug肯定是我们自己没用对导致的!想象一下也知道,别个都在一线大型互联网项目实战那么久了,岂能因你使用有误而被轻易推翻的?

1.3 搞懂Druid这几个核心参数用法很关键

本文重点介绍Druid数据源的如下几个关键的核心参数,搞懂它,一定让你真正玩好项目!不会再因为上述问题而烦恼:

  1. validationQuery
  2. testWhileIdle
  3. minEvictableIdleTimeMillis
  4. timeBetweenEvictionRunsMillis
  5. keepAlive
  6. keepAliveBetweenTimeMillis

2. Druid连接池6个核心参数详解

Druid数据源其实非常优秀的数据源,带有PreparedStatement缓存机制,性能非常高!但经常因为我们自己用不好,或是没有搞懂关于它的一些核心配置,从而导致恶性事件重复不止!

认真阅读本文,会让你接触上述困惑,只要搞懂这八个核心参数的组合使用,你不会再遇到上述druid连接池瘫痪的情况!

2.1 validationQuery

官方解释:用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。

请注意,这个参数必须配置,而且配置了才会让testOnBorrow、testOnReturn、testWhileIdle这几个参数生效。如果不配,上述数据库宕机事故,druid连接池依然无法自动恢复正常的!

2.2 validationQueryTimeout

官方解释:单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法。

该参数是配合上一个validationQuery一起使用的,不能太大,太大,会让检测时间太久,连接池恢复正常耗时很长!设置1-3秒比较合适。

2.3 testWhileIdle

官方解释:申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。建议配置为true,不影响性能,并且保证安全性。

官方实现代码如下:

public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException {
        int notFullTimeoutRetryCnt = 0;
        for (; ; ) {
            // handle notFullTimeoutRetry
            DruidPooledConnection poolableConnection;
            try {
                poolableConnection = getConnectionInternal(maxWaitMillis);
            } catch (GetConnectionTimeoutException ex) {
                if (notFullTimeoutRetryCnt < this.notFullTimeoutRetryCount && !isFull()) {
                    notFullTimeoutRetryCnt++;
                    if (LOG.isWarnEnabled()) {
                        LOG.warn("get connection timeout retry : " + notFullTimeoutRetryCnt);
                    }
                    continue;
                }
                throw ex;
            }

            if (testOnBorrow) {
                boolean validated = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
                if (!validated) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("skip not validated connection.");
                    }

                    discardConnection(poolableConnection.holder);
                    continue;
                }
            } else {
                if (poolableConnection.conn.isClosed()) {
                    discardConnection(poolableConnection.holder); // 传入null,避免重复关闭
                    continue;
                }

                if (testWhileIdle) {
                    final DruidConnectionHolder holder = poolableConnection.holder;
                    long currentTimeMillis = System.currentTimeMillis();
                    long lastActiveTimeMillis = holder.lastActiveTimeMillis;
                    long lastExecTimeMillis = holder.lastExecTimeMillis;
                    long lastKeepTimeMillis = holder.lastKeepTimeMillis;

                    if (checkExecuteTime
                            && lastExecTimeMillis != lastActiveTimeMillis) {
                        lastActiveTimeMillis = lastExecTimeMillis;
                    }

                    if (lastKeepTimeMillis > lastActiveTimeMillis) {
                        lastActiveTimeMillis = lastKeepTimeMillis;
                    }

                    long idleMillis = currentTimeMillis - lastActiveTimeMillis;

                    if (idleMillis >= timeBetweenEvictionRunsMillis
                            || idleMillis < 0 // unexcepted branch
                    ) {
                        boolean validated = testConnectionInternal(poolableConnection.holder, poolableConnection.conn);
                        if (!validated) {
                            if (LOG.isDebugEnabled()) {
                                LOG.debug("skip not validated connection.");
                            }

                            discardConnection(poolableConnection.holder);
                            continue;
                        }
                    }
                }
            }

            if (removeAbandoned) {
                StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
                poolableConnection.connectStackTrace = stackTrace;
                poolableConnection.setConnectedTimeNano();
                poolableConnection.traceEnable = true;

                activeConnectionLock.lock();
                try {
                    activeConnections.put(poolableConnection, PRESENT);
                } finally {
                    activeConnectionLock.unlock();
                }
            }

            if (!this.defaultAutoCommit) {
                poolableConnection.setAutoCommit(false);
            }

            return poolableConnection;
        }
    }

这个就按官方建议来吧,配置为true!

2.4 minEvictableIdleTimeMillis

官方解释:连接保持空闲而不被驱逐的最小时间,即最小生存时间!见如下代码:

for (; i < poolingCount; ++i) {
                DruidConnectionHolder connection = connections[i];

                if ((onFatalError || fatalErrorIncrement > 0) && (lastFatalErrorTimeMillis > connection.connectTimeMillis)) {
                    keepAliveConnections[keepAliveCount++] = connection;
                    continue;
                }

                if (checkTime) {
                    if (phyTimeoutMillis > 0) {
                        long phyConnectTimeMillis = currentTimeMillis - connection.connectTimeMillis;
                        if (phyConnectTimeMillis > phyTimeoutMillis) {
                            evictConnections[evictCount++] = connection;
                            continue;
                        }
                    }

                    long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis;

                    if (idleMillis < minEvictableIdleTimeMillis
                            && idleMillis < keepAliveBetweenTimeMillis) {
                        break;
                    }

                    if (idleMillis >= minEvictableIdleTimeMillis) {
                        if (i < checkCount) {
                            evictConnections[evictCount++] = connection;
                            continue;
                        } else if (idleMillis > maxEvictableIdleTimeMillis) {
                            evictConnections[evictCount++] = connection;
                            continue;
                        }
                    }

                    if (keepAlive && idleMillis >= keepAliveBetweenTimeMillis
                            && currentTimeMillis - connection.lastKeepTimeMillis >= keepAliveBetweenTimeMillis) {
                        keepAliveConnections[keepAliveCount++] = connection;
                    } else {
                        if (i != remaining) {
                            // move the connection to the new position for retaining it in the pool.
                            connections[remaining] = connection;
                        }
                        remaining++;
                    }
                } else {
                    if (i < checkCount) {
                        evictConnections[evictCount++] = connection;
                    } else {
                        break;
                    }
                }
            }

销毁连接时,当检测到当前连接的最后活动时间当前时间差(即连接的空闲时间)大于该值时,关闭当前连接。

连接的空闲时间大于 minEvictableIdleTimeMillis(连接保持空闲而不被驱逐的最小时间), 则进行回收。

2.5 timeBetweenEvictionRunsMillis

官方解释:

有两个含义:

  1. Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。
  2. testWhileIdle的判断依据,详细看testWhileIdle属性的说明。

正确理解:

  1. 如果testWhileIdle为true,距离上次激活时间超过timeBetweenEvictionRunsMillis,则进行连接有效性检测,即执行validationQuery检测连接是否有效。
  2. 连接销毁线程(DestroyConnectionThread)该线程主要工作是将空闲及无效的连接销毁,可以通过timeBetweenEvictionRunsMillis 时间设置执行间隔。每次回收都是从connects 头部开始遍历;

DestroyConnectionThread线程主要回收以下几类连接:

  1. 连接的空闲时间大于 minEvictableIdleTimeMillis(连接保持空闲而不被驱逐的最小时间), 则进行回收。
  2. 大于minIdle 部分的连接会被回收。保证连接池空闲连接不会太多。
  3. 检查连接活跃度,不健康的连接则关闭。默认不检查,可以通过 druid.keepAlive 打开连接的健康检查。

2.6 keepAliveBetweenTimeMillis

官方解释:单位毫秒,若连接空闲时间大于keepAliveBetweenTimeMillis毫秒,执行一次有效性检测,检测不通过的连接会被销毁。

官方代码如下:

protected void createAndStartDestroyThread() {
        destroyTask = new DestroyTask();

        if (destroyScheduler != null) {
            long period = timeBetweenEvictionRunsMillis;
            if (period <= 0) {
                period = 1000;
            }
            destroySchedulerFuture = destroyScheduler.scheduleAtFixedRate(destroyTask, period, period,
                    TimeUnit.MILLISECONDS);
            return;
        }

        String threadName = "Druid-ConnectionPool-Destroy-" + System.identityHashCode(this);
        destroyConnectionThread = new DestroyConnectionThread(threadName);
        destroyConnectionThread.start();
    }

从上述代码可知,参数timeBetweenEvictionRunsMillis仅仅是销毁线程DestroyConnectionThread的运行周期!

Druid数据库连接池中还有一个销毁连接的线程,上述已提到,名叫DestroyConnectionThread,该线程会每间隔timeBetweenEvictionRunsMillis的时间执行一次DestroyTask任务来销毁连接,这些被销毁的连接可以是存活时间达到最大值的连接,也可以是空闲时间达到指定值的连接。

如果还开启了保活机制,那么空闲时间大于keepAliveBetweenTimeMillis的连接都会被校验一次有效性,校验不通过的连接会被销毁。

因此keepAliveBetweenTimeMillis一定要大于timeBetweenEvictionRunsMillis,前者是连接保持存货的最小生命值(毫秒),而后者是检测该生命值的线程的运行时间间隔!

3. 项目实战中Druid连接池的配置

以下是项目实战中的Druid连接池可靠性配置,可以保证数据库宕机重启后,应用无需重启都能很快恢复正常响应状态!根据实际需要,只需要修改maxActive值即可!

      # 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
      initialSize: 8
      
      # 连接池最小空闲的连接数
      minIdle: 8
      
      # 连接池最大“活跃”连接数量,当连接数量达到该值时,再获取新连接时,将处于等待状态,直到有连接被释放,才能借用成功
      maxActive: 512
      
      # 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
      maxWait: 30000
      
      # 连接池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作。实际项目中建议配置成true
      keepAlive: true
      
      # 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
      validationQuery: "select 'x'"
      
      # 单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法
      validationQueryTimeout: 3
      
      # 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
      testWhileIdle: true
      
      # 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
      testOnBorrow: false
      
      # 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
      testOnReturn: false
      
      # 默认自动提交
      default-auto-commit: true
      
      # 默认只读
      default-read-only: false
      
      # 默认事务隔离
      default-transaction-isolation: 4
      
      # 销毁连接时检测当前连接的最后活动时间和当前时间差大于该值时,关闭当前连接(配置连接在池中的最小生存时间)
      # 配置一个连接在池中最小生存的时间(连接保持空闲而不被驱逐的最小时间),单位是毫秒
      minEvictableIdleTimeMillis: 1800000
      
      #  有两个含义:
      #  1) Destroy线程会检测连接的间隔时间(即Druid数据库连接池有一个销毁连接的线程会每间隔timeBetweenEvictionRunsMillis执行一次DestroyTask#run方法来销毁连接),如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。
      #  2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明 
      timeBetweenEvictionRunsMillis: 15000
      
      # 空闲时间大于30秒,执行一次有效性检测,检测不通过的连接会被销毁
      # Druid数据库连接池中还有一个销毁连接的线程,会每间隔timeBetweenEvictionRunsMillis的时间执行一次DestroyTask任务来销毁连接,这些被销毁的连接可以是存活时间达到最大值的连接,也可以是空闲时间达到指定值的连接。如果还开启了保活机制,那么空闲时间大于keepAliveBetweenTimeMillis的连接都会被校验一次有效性,校验不通过的连接会被销毁。
      keepAliveBetweenTimeMillis: 30000
      
      # 打开PSCache,并且指定每个连接上PSCache的大小
      # 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
      poolPreparedStatements: true
      
      # 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
      max-pool-prepared-statement-per-connection-size: 20

4. 总结

本文中,我们详细介绍了Druid连接池的几个核心参数配置,通过对这些配置介绍,能使我们能更准确的了解Druid,然后感受Druid的高性能,可靠性设计的迷人之处!支持国产数据源,支持Druid!


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。