这几天在研究各个数据库连接池,比如ali druid, dbcp2 以及最近很火的连接池 HikariCP, 除了常规的池化连接对象管理外。关键区别就是如何创建连接,防止连接泄漏,如何获取连接这些细节的区分点.在hikariCP的wiki中,提到一篇文章 https://github.com/brettwooldridge/HikariCP/wiki/Bad-Behavior:-Handling-Database-Down, 这里面提到由于网络的问题导致获取连接会比预期的时间长的这一问题。这也是我们为什么在选择一些组件和框架时,均会优先使用偏新的版本的原因。一些老的,旧的连接池,因为api的限制,对一些极端情况的处理并不能如意。比如在测试中的c3p0,dbcp这些常规连接池,由于历史原因,在处理一些新的需求时都不能满足需求。
我们来看对于一个标准的数据库连接,我们会有以下的要求:
- 在规定的时间内拿到数据库连接
- 使用一个alive的connection进行数据访问
整个要求其实就是判断connection存活,处理超时的问题。在常规的数据访问中,因为不同sql查询的原因,我们不能够期望数据库查询在一个较小的时间内就一定能返回(比如5秒)。但是在程序中,当获取一个连接时,又期望在较小的时间内返回给应用,以方便应用能够快速响应业务。如果5s之内(或者更小)不能拿到连接,能认为当前业务失败,避免业务长时间不能响应。甚至避免整个系统所有线程均blocking在获取数据库连接这一步,而导致系统不可用.
那么整个问题变为了以下3步:
- 业务在较小的时间内拿到连接,如果超时则立刻返回
- 连接池在较小的时间内创建数据库连接,如果超时则立即返回,进行下一步尝试
- 连接池在较小的时间内验证已经创建好的连接当前可用,如果超时则立即返回,并标记当前连接不可用,选择其它的连接
业务中获取连接 maxWait
在业务中获取连接对象时,即通过datasource.getConnection()来获取,因为参数中无法传递额外的时间参数,一般是提前在相应的datasource实现中配置wait参数。比如在druid中可以配置maxWait参数,hikari可以配置connectionTimeout参数用于控制客户端获取连接的超时时间.
如果第一次数据源还没有活跃的连接,则需要进行创建连接或者验证已有的连接是否有效这些操作。一般情况下,这些流程可能会比maxWait的时间更长,如果采用阻塞式地操作,那么这种情况下maxWait即没有实际的作用。而导致实际上超过maxWait的时间情况下业务中还没有响应结果的情况。
这种处理方式,可以采用,获取连接和数据源创建连接两个事务分开的作法,即通过2个线程,使用线程协作的方式来处理。具体处理方法参考如下
- 定义条件信号量signal
- 获取连接线程处理,如果当前不能立即拿到连接(使用无等待逻辑), 则通知创建线程进行连接创建,并且信号条件condition.await(maxWait)
- 创建连接线程收到信号量,启动创建连接的流程,如果能够创建成功,则通过信号量反馈连接线程。signal.notify()
- 连接线程如果在maxWait时间内得到通知,则返回业务。否则,await将在 maxWait之后,唤醒线程。在这种情况下,业务线程将在最多maxWait时间之内即能够响应业务.
线程协作,即是利用condition条件变量来完成业务的等待和唤醒处理,即一种将阻塞式调用更换为异步回调式调用。
创建数据库连接 connectTimeout
数据库连接池在获取物理连接时,通过调用 Connection Driver.connect(String url, Properties info) 来创建连接。在这个接口中, 并没有提供像连接超时的参数。不过对于像这个情况,针对于mysql数据库,可以有以下两种方式。
- 通过DriverManager.setLoginTimeout(int) 设置登陆超时时间。此方法设置的值,可以通过 DriverManager.getLoginTimeout() 来获取。此获取的值的具体使用则是由各个数据库driver来决定的。 此方法设置可能无效…
- mysql数据库连接参数中,可以在url或properties中配置参数, connectTimeout(毫秒值)。此参数的目的用于配置在mysql中连接数据库的超时时间。
在当前的业务实现中,mysql会同时读取connectTime和loginTimeout的参数值,并且以两者之间的最小值为最终配置值.(如果只有1个配置,则用其中的一个).这里面有1个细节,即是loginTimeout的配置值在mysql中只会读取一次,因此必须在调用mysql创建连接时应该尽早地设置此参数。
在实际的下层实现中,最终的connectTimeout会绑定在物理连接上的socket连接对象上。在标准的java socket中,在连接时,可以通过 socket.connect(socketAddress address, int timeout) 来处理连接的问题,即最终将connectTimeout转换为socket.connect时的timeout参数。 而如果没有提供此值时,则默认为无限阻塞,具体阻塞的时长由操作系统来决定,这就不能保证了.
验证数据库连接 ValidationQueryTimeout
相应的connection被池化到datasource中的连接池中,当出现网络错误或者其它问题导致实际的连接不可用时,在java中的connection其实是不能感知的。因此当从池中get一个connection时,需要进行再次验证以保证这个连接是可用的。在相应的连接池框架中,均使用了类似testOnBorrow来处理。testOnBorrow则需要额外的validationQuery参数,以表示通过哪个查询来验证对象是否可用。
通常情况下,验证连接是否可用,即使用此connection对象发一个最简单的查询语句,如果能够拿到结果没有报错,则表示此连接就是有效的。
但是在此场景下,如果查询很久没有返回结果,则当前验证查询即被阻塞住,进而进一步阻塞业务。通过validatequeryTimeout参数可以处理此问题。此参数用于控制在验证查询时,相应的查询超时时间,在相应的api实现上。可以有以下两种方法来处理此问题
- 在进行查询时使用PreparedStatement.setQueryTimeout 设置查询超时时间.此参数控制每一次查询时的超时时间
- 在相应的connection对象上,通过Connection.setNetworkTimeout(executor, timeout) 来设置网络超时时间。此参数用于读取数据时的超时时间。需要注意的是此参数从jdk1.7才提供,属于jdbc 4.0中的一部分。
在具体的连接池实现上,ali druid使用第1种方案。而hikari则混合使用两种方案,即优先使用第2种,如果不支持,则使用第1种。
在底层的实现上,最终的设置均反应到socket对象上的特定属性soTimeout,即通过调用socket.setSoTimeout(timeout) 来实现此效果。当调用了此方法之后,后续的inputStream.read方法将受到此timeout的影响,当超过了timeout参数之后,相应的read即会throw SocketTimeoutException异常。而此api最终体现到tcp中的so_timeout参数.
需要注意的是,因此此参数是影响整个connection的,因此如果此参数设置之后,后续的其它业务查询也会受影响,导致一些长时间的查询直接报错。因此,标准的处理方式,即先get到原来的旧时间设置,即通过 connection.getNetworkTimeout获取旧值,在查询了验证查询之后,再次调用connectin.setNetworkTimeout设置回旧值。 而如果在这个过程中有错误,则直接表示连接不可用,则不需要再设置旧值了.
mysql中的validation
由于标准的select 1 这种验证查询语句总是要走一次完整的发送,解析,查询,返回结果,以及构建协议结果等。相对来说还是有点浪费资源,因此在mysql的版本中,mysql自动提供了一个更为简单的协议语句,即ping指令。通过发送ping请求来探测网络的状态。相应的api为 ConnectionImpl.pingInternal(checkForClosedConnection, timeoutMillis) 此方法同样接收timeout参数,同样最终反应到socket的sotimeout中。此方法相对于mysql版本来说,是实现连接探测的更好选择(相比query).
总结
在相应的数据库连接池中,在开放的api中,开始逐渐提供更完善的网络参数定义,以满足更加精细地网络需求。在开发与网络相关的应用中,既然是简单的数据库连接操作,也需要关注这些信息。它关系到整个系统是否会受到阻塞式处理导致失去响应的情况,同时也是评价一个第三方组件是否合格的重要参数。而近来网络开发越来越流行,了解这些下层的实现,也有助于在上层api受限的情况下,进一步满足系统的需要,也提供一些封装上层实现的参考。
转载请标明出处:i flym
本文地址:https://www.iflym.com/index.php/code/201805160001.html