连接池 Druid (四) - 连接归还

2023-12-06 03:36
文章标签 连接 连接池 druid 归还

本文主要是介绍连接池 Druid (四) - 连接归还,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

轻车熟路,连接归还是通过Connection的代理对象重写close方法完成的,通过前面的学习我们已经知道Connectin的代理对象是DruidPooledConnection,所以我们直接看DruidPooledConnection的close方法。

DruidPooledConnection#close

直接上代码:

   public void close() throws SQLException {if (this.disable) {return;}DruidConnectionHolder holder = this.holder;if (holder == null) {if (dupCloseLogEnable) {LOG.error("dup close");}return;}DruidAbstractDataSource dataSource = holder.getDataSource();boolean isSameThread = this.getOwnerThread() == Thread.currentThread();if (!isSameThread) {dataSource.setAsyncCloseConnectionEnable(true);}if (dataSource.isAsyncCloseConnectionEnable()) {syncClose();return;}

判断当前Connection(DruidPooledConnection)的状态为disbale、或者connectionHolder(DruidConnectionHolder)为null(说明连接已关闭了),则直接返回。

然后判断连接的ownerThread(获取connection的线程)与当前线程不是同一线程的话,则设置异步关闭asyncCloseConnectionEnable=true。调用syncClose()方法。

syncClose()方法和同线程关闭方式的代码逻辑基本一致,只不过syncClose()整个方法需要加锁,同线程关闭则不需要。

所以我们就不贴出syncClose()方法的源码了。

在什么场景下数据库连接会跨线程关闭?一个线程获取到数据库连接,然后会交给另外一个线程,由另外一个线程执行连接关闭?应用层这么做是为了实现多线程之间的事务处理?

接下来:

       if (!CLOSING_UPDATER.compareAndSet(this, 0, 1)) {return;}try {for (ConnectionEventListener listener : holder.getConnectionEventListeners()) {listener.connectionClosed(new ConnectionEvent(this));}List<Filter> filters = dataSource.getProxyFilters();if (filters.size() > 0) {FilterChainImpl filterChain = new FilterChainImpl(dataSource);filterChain.dataSource_recycle(this);} else {recycle();}} finally {CLOSING_UPDATER.set(this, 0);}this.disable = true;}

CAS方式修改当前对象的closing属性值为1(0->1),如果修改不成功,则说明有其他线程正在试图关闭当前连接,直接返回。

获取ConnectionHolder的所有ConnectionEventListener对象,给所有监听对象发送连接关闭通知。

然后,获取dataSource的ProxyFilters,不空的话调用filterChain的dataSource_recycle方法,有关dataSource过滤器我们依然暂时不管。没有filter的话,调用recycle()回收连接,之后CAS方法修改closing属性值为0,并设置当前连接对象的disable=true。

所以,我们发现连接归还应该是recycle()方法中实现的。

DruidPooledConnection#recycle()

连接(DruidPooledConnection)的recycle方法:

   public void recycle() throws SQLException {if (this.disable) {return;}DruidConnectionHolder holder = this.holder;if (holder == null) {if (dupCloseLogEnable) {LOG.error("dup close");}return;}if (!this.abandoned) {DruidAbstractDataSource dataSource = holder.getDataSource();dataSource.recycle(this);}this.holder = null;conn = null;transactionInfo = null;closed = true;}

如果当前连接没有被遗弃(abandoned)的话,调用dataSource的recycle方法回收,之后清理相关对象、设置close为true。

abandoned是在removeAbandoned参数打开、连接执行时长超过设定的超时时长removeAbandonedTimeoutMillis之后设置的。有关removeAbandoned我们后面会专门进行分析,此处略过。

那接下来就是DataSource的recycle方法了。

DruidDataSrouce#recycle

代码比较长,我们还是分段分析:

/*** 回收连接*/protected void recycle(DruidPooledConnection pooledConnection) throws SQLException {final DruidConnectionHolder holder = pooledConnection.holder;if (holder == null) {LOG.warn("connectionHolder is null");return;}if (logDifferentThread //&& (!isAsyncCloseConnectionEnable()) //&& pooledConnection.ownerThread != Thread.currentThread()//) {LOG.warn("get/close not same thread");}final Connection physicalConnection = holder.conn;if (pooledConnection.traceEnable) {Object oldInfo = null;activeConnectionLock.lock();try {if (pooledConnection.traceEnable) {oldInfo = activeConnections.remove(pooledConnection);pooledConnection.traceEnable = false;}} finally {activeConnectionLock.unlock();}if (oldInfo == null) {if (LOG.isWarnEnabled()) {LOG.warn("remove abandonded failed. activeConnections.size " + activeConnections.size());}}}

吐槽一句,终于出现了哪怕是一句话的javaDoc说明:回收连接。整个Druid连接池的源码中,都非常吝啬,少有注释。

判断当前连接DruidPooledConnection的traceEnable属性为true的话,加activeConnectionLock锁之后,将当前连接从activeConnections中移除。

traceEnable属性其实反应的还是removeAbandoned参数(这个参数虽然在正式环境不建议打开,但是代码中阴魂不散,到处都有)。removeAbandoned参数打开的情况下,获取连接的时候会将连接放入activeConnections中,并同时设置连接的traceEnable为true。所以我们其实可以认为这个traceEnable其实就是removeAbandoned参数,换了个名字而已。

所以这里的逻辑是:removeAbandoned参数打开的话,连接归还的时候如果该连接没有被abandoned的话,在归还连接时会将当前连接从activeConnections中移除,该连接就会避免被abandoned处理。

接下来:

         final boolean isAutoCommit = holder.underlyingAutoCommit;final boolean isReadOnly = holder.underlyingReadOnly;final boolean testOnReturn = this.testOnReturn;try {// check need to rollback?if ((!isAutoCommit) && (!isReadOnly)) {pooledConnection.rollback();}   boolean isSameThread = pooledConnection.ownerThread == Thread.currentThread();if (!isSameThread) {final ReentrantLock lock = pooledConnection.lock;lock.lock();try {holder.reset();} finally {lock.unlock();}} else {holder.reset();}if (holder.discard) {return;}if (phyMaxUseCount > 0 && holder.useCount >= phyMaxUseCount) {discardConnection(holder);return;}if (physicalConnection.isClosed()) {lock.lock();try {if (holder.active) {activeCount--;holder.active = false;}closeCount++;} finally {lock.unlock();}return;}

对没有提交的事务做回滚处理。之后调用DruidConnectionHolder的reset方法,重新设置连接属性为默认值,因为连接获取之后应用从根据需要可能对连接属性重新进行了设置,归还的时候重新设置会默认值,以便每一次应用获取连接之后都能拿到各项属性设置为默认值的连接、而不是不确定。

这里对没有提交的事务的判断条件是!isAutoCommit,获取的是物理连接Connection的isAutocommit属性。我们知道如果应用开启事务管理的话,获取连接之后会设置连接的autoCommit为false,事务提交或回滚之后,一般情况下应用也会恢复连接的默认设置,这个时候autoCommit就会被恢复为true。比如Spring的事务框架在事务提交之后会通过TransactionManager的cleanupAfterCompletion->doCleanupAfterCompletion设置autoCommit为true。所以正常来讲,应用已经提交事务之后,autoCommit就会变为true,也就不需要Druid在归还连接的时候处理回滚了。

Druid增加这个判断可能是为了给应用擦屁股(猜测而已),应用层没有启用事务框架的情况下手动开启了事务、设置autoCommit为false,执行完成之后没有重置autoCommit,这个时候应用可能commit、也可能没有commit事务,Durid的处理方式是:一律回滚。

仔细检查了HikariPool的连接回收过程,并没有这个回滚处理。个人不赞成(基于猜测而已………)Druid的这个回滚处理,因为事务提交还是回滚终究还是应用应该关注的事情,应用层为了简化处理逻辑可以把事务管理交给框架,比如Spring事务框架来处理。也就是说,要么是应用层自己解决、要么交给事务框架解决事务的提交或回滚是比较合理的选择。

继续。

检查如果DruidConnectionHolder已经被弃用(连接已经被弃用),直接返回,不归还。

检查如果物理连接已经被关闭,则加锁修改当前holder的active状态为false,直接返回,不归还。

然后:

           if (testOnReturn) {boolean validate = testConnectionInternal(holder, physicalConnection);if (!validate) {JdbcUtils.close(physicalConnection);destroyCountUpdater.incrementAndGet(this);lock.lock();try {if (holder.active) {activeCount--;holder.active = false;}closeCount++;} finally {lock.unlock();}return;}}if (holder.initSchema != null) {holder.conn.setSchema(holder.initSchema);holder.initSchema = null;}if (!enable) {discardConnection(holder);return;}

检查testOnReturn参数如果设置为true,则测试连接可用性,如果连接不可用,处理方式同上,修改holder的active=false,直接返回,不归还。

testOnReturn参数也没有必要设置,道理和testOnBorrow参数一样,会影响性能。这两个参数的默认设置都是false,不打开。

之后检查当前dataSrouce如果不可用,则调用discardConnection关闭连接,不归还。

然后:

            boolean result;final long currentTimeMillis = System.currentTimeMillis();if (phyTimeoutMillis > 0) {long phyConnectTimeMillis = currentTimeMillis - holder.connectTimeMillis;if (phyConnectTimeMillis > phyTimeoutMillis) {discardConnection(holder);return;}}lock.lock();try {if (holder.active) {activeCount--;holder.active = false;}closeCount++;result = putLast(holder, currentTimeMillis);recycleCount++;} finally {lock.unlock();}if (!result) {JdbcUtils.close(holder.conn);LOG.info("connection recyle failed.");}} catch (Throwable e) {holder.clearStatementCache();if (!holder.discard) {discardConnection(holder);holder.discard = true;}LOG.error("recyle error", e);recycleErrorCountUpdater.incrementAndGet(this);}}

如果设置了phyTimeoutMillis(默认设置为-1,不检查)的话,检查当前连接创建以来的时长是否超过了该参数的设置,超过的话关闭连接,不归还。这个参数也不建议设置。

之后,加锁,调用putLast(holder, currentTimeMillis);归还连接,如果归还失败,则调用JdbcUtil.close方法,底层直接关闭连接。

putLast是归还连接的核心方法。

DruidDataSource#putLast

putLast是连接归还的最后一步,目的是各种检查校验、连接属性重置之后,最终将连接放回到连接池connections中。

putLast是在锁状态下执行的。

看代码:

   boolean putLast(DruidConnectionHolder e, long lastActiveTimeMillis) {if (poolingCount >= maxActive || e.discard || this.closed) {return false;}e.lastActiveTimeMillis = lastActiveTimeMillis;connections[poolingCount] = e;incrementPoolingCount();if (poolingCount > poolingPeak) {poolingPeak = poolingCount;poolingPeakTime = lastActiveTimeMillis;}notEmpty.signal();notEmptySignalCount++;return true;}

检查如果没必要归还的话,比如连接池数量大于参数设定的maxActive数量、或者当前连接已经被遗弃、或者当前dataSource已经被关闭,则不需要归还,直接返回。

将连接放入到connections的尾部,之后增加连接池数量poolingCount。

调用notEmpty.signal();唤醒等待获取连接的线程:连接池中有新的连接加入,可以获取了。

Druid关闭连接(归还连接)代码分析完毕!

小结

Druid连接池的初始化、连接获取以及连接归还的源码分析完毕,后面会补充一篇文章分析连接遗弃的处理。

Thanks a lot!

上一篇 连接池 Druid (三) - 获取连接 getConnection
下一篇 连接池 Druid (补充) - removeAbandoned

这篇关于连接池 Druid (四) - 连接归还的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/460271

相关文章

Xshell远程连接失败以及解决方案

《Xshell远程连接失败以及解决方案》本文介绍了在Windows11家庭版和CentOS系统中解决Xshell无法连接远程服务器问题的步骤,在Windows11家庭版中,需要通过设置添加SSH功能并... 目录一.问题描述二.原因分析及解决办法2.1添加ssh功能2.2 在Windows中开启ssh服务2

Redis连接失败:客户端IP不在白名单中的问题分析与解决方案

《Redis连接失败:客户端IP不在白名单中的问题分析与解决方案》在现代分布式系统中,Redis作为一种高性能的内存数据库,被广泛应用于缓存、消息队列、会话存储等场景,然而,在实际使用过程中,我们可能... 目录一、问题背景二、错误分析1. 错误信息解读2. 根本原因三、解决方案1. 将客户端IP添加到Re

Mysql 中的多表连接和连接类型详解

《Mysql中的多表连接和连接类型详解》这篇文章详细介绍了MySQL中的多表连接及其各种类型,包括内连接、左连接、右连接、全外连接、自连接和交叉连接,通过这些连接方式,可以将分散在不同表中的相关数据... 目录什么是多表连接?1. 内连接(INNER JOIN)2. 左连接(LEFT JOIN 或 LEFT

Spring Boot实现多数据源连接和切换的解决方案

《SpringBoot实现多数据源连接和切换的解决方案》文章介绍了在SpringBoot中实现多数据源连接和切换的几种方案,并详细描述了一个使用AbstractRoutingDataSource的实... 目录前言一、多数据源配置与切换方案二、实现步骤总结前言在 Spring Boot 中实现多数据源连接

QT实现TCP客户端自动连接

《QT实现TCP客户端自动连接》这篇文章主要为大家详细介绍了QT中一个TCP客户端自动连接的测试模型,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录版本 1:没有取消按钮 测试效果测试代码版本 2:有取消按钮测试效果测试代码版本 1:没有取消按钮 测试效果缺陷:无法手动停

W外链微信推广短连接怎么做?

制作微信推广链接的难点分析 一、内容创作难度 制作微信推广链接时,首先需要创作有吸引力的内容。这不仅要求内容本身有趣、有价值,还要能够激起人们的分享欲望。对于许多企业和个人来说,尤其是那些缺乏创意和写作能力的人来说,这是制作微信推广链接的一大难点。 二、精准定位难度 微信用户群体庞大,不同用户的需求和兴趣各异。因此,制作推广链接时需要精准定位目标受众,以便更有效地吸引他们点击并分享链接

Java 连接Sql sever 2008

Java 连接Sql sever 2008 /Sql sever 2008 R2 import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; public class TestJDBC

实例:如何统计当前主机的连接状态和连接数

统计当前主机的连接状态和连接数 在 Linux 中,可使用 ss 命令来查看主机的网络连接状态。以下是统计当前主机连接状态和连接主机数量的具体操作。 1. 统计当前主机的连接状态 使用 ss 命令结合 grep、cut、sort 和 uniq 命令来统计当前主机的 TCP 连接状态。 ss -nta | grep -v '^State' | cut -d " " -f 1 | sort |

如何确定 Go 语言中 HTTP 连接池的最佳参数?

确定 Go 语言中 HTTP 连接池的最佳参数可以通过以下几种方式: 一、分析应用场景和需求 并发请求量: 确定应用程序在特定时间段内可能同时发起的 HTTP 请求数量。如果并发请求量很高,需要设置较大的连接池参数以满足需求。例如,对于一个高并发的 Web 服务,可能同时有数百个请求在处理,此时需要较大的连接池大小。可以通过压力测试工具模拟高并发场景,观察系统在不同并发请求下的性能表现,从而

【Go】go连接clickhouse使用TCP协议

离开你是傻是对是错 是看破是软弱 这结果是爱是恨或者是什么 如果是种解脱 怎么会还有眷恋在我心窝 那么爱你为什么                      🎵 黄品源/莫文蔚《那么爱你为什么》 package mainimport ("context""fmt""log""time""github.com/ClickHouse/clickhouse-go/v2")func main(