深入理解JDBC的超时设置
深入理解 JDBC 的超时设置
原文:http://www.cubrid.org/blog/dev-platform/understanding-jdbc-internals-and-timeout-configuration/
恰当的 JDBC 超时设置能够有效地减少服务失效的时间。本文将对数据库的各种超时设置及其配置方法进行详细介绍。
真实案例:DDos 攻击后的服务不可用
在一次遭到 DDos 攻击后,整个服务陷入瘫痪。由于第四层交换机不堪重负,网络变得无法连接,导致业务系统也无法正常运转。安全组很快屏蔽了所有的 DDos 攻击并恢复了网络,但业务系统却依然无法工作。
通过分析系统的 Thread Dump 发现,业务系统停在了 JDBC API 的调用上。20 分钟后,系统仍处于 WAITING 状态,无法响应;直到 30 分钟后,系统抛出异常,服务才恢复正常。
为什么我们明明将 Query Timeout 设置成了 3 秒,系统却持续了 30 分钟的 WAITING 状态?为什么 30 分钟后系统又恢复正常了?当你理解了 JDBC 的超时设置机制后,就能找到问题的答案。
为什么我们要了解 JDBC
当遇到性能问题或系统出错时,业务系统和数据库通常是我们最关心的两个部分。在企业架构中,这两部分通常由不同的部门负责,因此各部门往往集中精力在自身领域内排查问题。这样一来,业务系统和数据库之间的部分就会成为一个盲区。对于 Java 应用而言,这个盲区就是 DBCP 数据库连接池和 JDBC,本文将集中介绍 JDBC。
什么是 JDBC
JDBC 是 Java 应用中用来连接关系型数据库的标准 API。Sun 公司定义了 4 种类型的 JDBC 驱动,我们主要使用的是第 4 种。该类型的 Driver 完全由 Java 代码实现,通过使用 Socket 与数据库进行通信。

图 1 JDBC Type 4.
第 4 种类型的 JDBC 通过 Socket 对字节流进行处理,因此涉及一些基本网络操作,类似于 HttpClient 这类网络通信库。当在网络操作中遇到问题时,将会消耗大量的 CPU 资源,并且导致响应超时。如果你之前用过 HttpClient,那么你一定遇到过未设置 Timeout 造成的错误。同样,第 4 种类型的 JDBC 若没有合理地设置 Socket Timeout,也会有相同的错误——连接被阻塞。
接下来,就让我们来学习一下如何正确地设置 Socket Timeout,以及需要考虑的问题。
应用与数据库间的超时层级

图 2 Timeout Class.
上图展示了简化后应用与数据库间的超时层级。(译者注:WAS/BLOC 是作者公司的具体应用名称,无需深究)高层级的超时设置依赖于底层级的超时设置,只有当底层级的超时设置无误时,高层级的超时才能确保正常。例如,当 Socket Timeout 出现问题时,高层级的 Statement Timeout 和 Transaction Timeout 都将失效。
我们收到的很多评论中提到:
即使设置了 Statement Timeout,当网络出错时,应用也无法从错误中恢复。
Statement Timeout 无法处理网络连接失败导致的超时,它能做的仅仅是限制 Statement 的操作时间。网络连接失败时的 Timeout 必须交由 JDBC 来处理。
JDBC 的 Socket Timeout 会受到操作系统 Socket Timeout 设置的影响,这就解释了为什么在之前的案例中,JDBC 连接会在网络出错后阻塞 30 分钟,然后又奇迹般恢复,即使我们并没有对 JDBC 的 Socket Timeout 进行设置。
DBCP 连接池位于图 2 的左侧,你会发现 Timeout 层级与 DBCP 是相互独立的。DBCP 负责的是数据库连接的创建和管理,并不干涉 Timeout 的处理。当连接在 DBCP 中创建,或是 DBCP 发送校验 Query 检查连接有效性的时候,Socket Timeout 将会影响这些过程,但并不直接对应用造成影响。
当在应用中调用 DBCP 的 getConnection() 方法时,你可以设置获取数据库连接的超时时间,但是这和 JDBC 的 Timeout 毫不相关。

图 3 Timeout for Each Levels.
什么是事务超时(Transaction Timeout)
Transaction Timeout 一般存在于框架(Spring, EJB)或应用级。
Transaction Timeout 或许是个相对陌生的概念,简单地说,Transaction Timeout 大致相当于 Statement Timeout × N(执行语句数量) + 其他业务逻辑耗时(如垃圾回收等)。Transaction Timeout 用来限制执行 Statement 的总时长。
例如,假设执行一个 Statement 需要 0.1 秒,那么执行少量 Statement 不会有什么问题,但若是要执行 100,000 个 Statement 则需要 10,000 秒(约 7 个小时)。这时,Transaction Timeout 就派上用场了。EJB CMT (Container Managed Transaction) 就是一种典型的实现,它提供了多种方法供开发者选择。但我们并不使用 EJB,Spring 的 Transaction Timeout 设置会更常用一些。在 Spring 中,你可以使用下面展示的 XML 或是在源码中使用 @Transactional 注解来进行设置。
<tx:attributes>
<tx:method name="…" timeout="3"/>
</tx:attributes> Spring 提供的 Transaction Timeout 配置非常简单,它会记录每个事务的开始时间和消耗时间,当特定的事件发生时就会对消耗时间做校验,当超出 Timeout 值时将抛出异常。
Spring 中,数据库连接被保存在 ThreadLocal 里,这被称为 事务同步(Transaction Synchronization),与此同时,事务的开始时间和消耗时间也被保存下来。当使用这种代理连接创建 Statement 时,就会校验事务的消耗时间。
EJB CMT 的实现方式与之类似,其结构本身也十分简单。当你选用的容器或框架并不支持 Transaction Timeout 这一特性,你可以考虑自己来实现。Transaction Timeout 并没有标准的 API。Lucy 框架的 1.5 和 1.6 版本都不支持 Transaction Timeout,但是你可以通过使用 Spring 的 Transaction Manager 来达到与之同样的效果。假设某个事务中包含 5 个 Statement,每个 Statement 的执行时间是 200ms,其他业务逻辑的执行时间是 100ms,那么 Transaction Timeout 至少应该设置为 1,100ms(200 * 5 + 100)。
什么是语句超时(Statement Timeout)
Statement Timeout 用来限制 Statement 的执行时长,Timeout 的值通过调用 JDBC 的 java.sql.Statement.setQueryTimeout(int timeout) API 进行设置。不过现在开发者已经很少直接在代码中设置,而多是通过框架来进行设置。
以 iBatis 为例,Statement Timeout 的默认值可以通过 sql-map-config.xml 中的 defaultStatementTimeout 属性进行设置。同时,你还可以设置 sqlmap 中 select、insert、update 标签的 timeout 属性,从而对不同 SQL 语句的超时时间进行独立的配置。如果你使用的是 Lucy 1.5 或 1.6 版本,通过设置 queryTimeout 属性可以在 datasource 层面对 Statement Timeout 进行设置。Statement Timeout 的具体值需要依据应用本身的特性而定,并没有可供推荐的配置。
不同驱动下的 Statement Timeout 处理机制
不同的关系型数据库,以及不同的 JDBC 驱动,其 Statement Timeout 处理过程会有所不同。其中,Oracle 和 MS SQLServer 的处理相类似,MySQL 和 CUBRID 类似。
Oracle JDBC Statement 的 QueryTimeout 处理过程
- 通过调用
Connection的createStatement()方法创建 Statement。 - 调用
Statement的executeQuery()方法。 - Statement 通过自身 Connection 将 Query 发送给 Oracle 数据库。
- Statement 在
OracleTimeoutPollingThread(每个 ClassLoader 一个)上进行注册。 - 达到超时时间。
OracleTimeoutPollingThread调用OracleStatement的cancel()方法。- 通过 Connection 向正在执行的 Query 发送 cancel 消息。

图 4 Query Timeout Execution Process for Oracle JDBC Statement.
JTDS (MS SQLServer) Statement 的 QueryTimeout 处理过程
- 通过调用
Connection的createStatement()方法创建 Statement。 - 调用
Statement的executeQuery()方法。 - Statement 通过自身 Connection 将 Query 发送给 MS SQLServer 数据库。
- Statement 在
TimerThread上进行注册。 - 达到超时时间。
TimerThread调用JtdsStatement实例中的TsdCore.cancel()方法。- 通过
ConnectionJDBC向正在执行的 Query 发送 cancel 消息。

图 5 QueryTimeout Execution Process for JTDS (MS SQLServer) Statement.
MySQL JDBC Statement 的 QueryTimeout 处理过程(5.0.8)
- 通过调用
Connection.createStatement()方法创建 Statement。 - 调用
Statement.executeQuery()方法。 - Statement 通过自身 Connection 将 Query 发送给 MySQL 数据库。
- Statement 创建一个新的
timeout-execution线程用于超时处理。 - 5.1 版本后改为每个 Connection 分配一个
timeout-execution线程。 - 向
timeout-execution线程进行注册。 - 达到超时时间。
timeout-execution线程创建一个和 Statement 配置相同的 Connection。- 使用新创建的 Connection 向超时 Query 发送 cancel query(
KILL QUERY "connectionId")。

图 6 QueryTimeout Execution Process for MySQL JDBC Statement (5.0.8).
CUBRID JDBC Statement 的 QueryTimeout 处理过程
- 通过调用
Connection的createStatement()方法创建 Statement。 - 调用
Statement的executeQuery()方法。 - Statement 通过自身 Connection 将 Query 发送给 CUBRID 数据库。
- Statement 创建一个新的
timeout-execution线程用于超时处理。 - 5.1 版本后改为每个 Connection 分配一个
timeout-execution线程。 - 向
timeout-execution线程进行注册。 - 达到超时时间。
- 超时处理线程调用驱动内部的 cancel 方法。
timeout-execution线程创建一个和 Statement 配置相同的 Connection。- 使用新创建的 Connection 向超时 Query 发送 cancel 消息。

图 7 QueryTimeout Execution Process for CUBRID JDBC Statement.
什么是 JDBC 的 Socket Timeout
第 4 种类型的 JDBC 使用 Socket 与数据库连接,数据库并不对应用与数据库间的连接超时进行处理。JDBC 的 Socket Timeout 在数据库被突然停掉或是发生网络错误(由于设备故障等原因)时十分重要。由于 TCP/IP 的结构原因,Socket 没有办法探测到网络错误,因此应用也无法主动发现数据库连接断开。如果没有设置 Socket Timeout 的话,应用在数据库返回结果前会无期限地等下去,这种连接被称为 Dead Connection。
为了避免 Dead Connections,Socket 必须要有超时配置。Socket Timeout 可以通过 JDBC 设置,它能够避免应用在发生网络错误时产生无休止等待的情况,缩短服务失效的时间。
不推荐使用 Socket Timeout 来限制 Statement 的执行时长,因此 Socket Timeout 的值必须要高于 Statement Timeout,否则,Socket Timeout 将会先生效,这样 Statement Timeout 就变得毫无意义,也无法生效。
下面展示了 Socket Timeout 的两个设置项,不同的 JDBC 驱动其配置方式会有所不同。
- Socket 连接时的 Timeout:通过
Socket.connect(SocketAddress endpoint, int timeout)设置。 - Socket 读写时的 Timeout:通过
Socket.setSoTimeout(int timeout)设置。
通过查看 CUBRID、MySQL、MS SQL Server (JTDS) 和 Oracle 的 JDBC 驱动源码,我们发现所有的驱动内部都是使用上面的 2 个 API 来设置 Socket Timeout 的。
下面是不同驱动的 Socket Timeout 配置方式。
| JDBC Driver | connectTimeout 配置项 | socketTimeout 配置项 | URL 格式 | 示例 |
|---|---|---|---|---|
| MySQL Driver | connectTimeout(默认值:0,单位:ms) | socketTimeout(默认值:0,单位:ms) | jdbc:mysql://[host:port],[host:port]…/[database][?propertyName1]=[propertyValue1][&propertyName2]=[propertyValue2]… | jdbc:mysql://xxx.xx.xxx.xxx:3306/database?connectTimeout=60000&socketTimeout=60000 |
| MS-SQL Driver jTDS Driver | loginTimeout(默认值:0,单位:s) | socketTimeout(默认值:0,单位:s) | jdbc:jtds:<server_type>://<server>[:<port>][/<database>][;<property>=<value>[;...]] | jdbc:jtds:sqlserver://server:port/database;loginTimeout=60;socketTimeout=60 |
| Oracle Thin Driver | oracle.net.CONNECT_TIMEOUT(默认值:0,单位:ms) | oracle.jdbc.ReadTimeout(默认值:0,单位:ms) | 不支持通过 URL 配置,只能通过 OracleDatasource.setConnectionProperties() API 设置,使用 DBCP 时可以调用 BasicDatasource.setConnectionProperties() 或 BasicDatasource.addConnectionProperties() 进行设置 | - |
| CUBRID Thin Driver | 无独立配置项(默认值:5,000,单位:ms) | 无独立配置项(默认值:5,000,单位:ms) | - | - |
connectTimeout和socketTimeout的默认值为 0 时,Timeout 不生效。- 除了调用 DBCP 的 API 以外,还可以通过 Properties 属性进行配置。
通过 Properties 属性进行配置时,需要传入 key 为 connectionProperties 的键值对,value 的格式为 [propertyName=property;]*。下面是 iBatis 中的 Properties 配置。
<transactionManager type="JDBC">
<dataSource type="com.nhncorp.lucy.db.DbcpDSFactory">
….
<property name="connectionProperties" value="oracle.net.CONNECT_TIMEOUT=6000;oracle.jdbc.ReadTimeout=6000"/>
</dataSource>
</transactionManager> 操作系统的 Socket Timeout 配置
如果不设置 Socket Timeout 或 Connect Timeout,应用多数情况下是无法发现网络错误的。因此,当网络错误发生后,在连接重新连接成功或成功接收到数据之前,应用会无限制地等下去。但是,通过本文开篇处的实际案例我们发现,30 分钟后应用的连接问题奇迹般的解决了,这是因为操作系统同样能够对 Socket Timeout 进行配置。公司的 Linux 服务器将 Socket Timeout 设置为了 30 分钟,从而会在操作系统的层面对网络连接做校验,因此即使 JDBC 的 Socket Timeout 设置为 0,由网络错误造成的数据库连接问题的持续时间也不会超过 30 分钟。
通常,应用会在调用 Socket.read() 时由于网络问题被阻塞住,而很少在调用 Socket.write() 时进入 WAITING 状态,这取决于网络构成和错误类型。当 Socket.write() 被调用时,数据被写入到操作系统内核的缓冲区,控制权立即回到应用手上。因此,一旦数据被写入内核缓冲区,Socket.write() 调用就必然会成功。但是,如果系统内核缓冲区由于某种网络错误而满了的话,Socket.write() 也会进入 WAITING 状态。这种情况下,操作系统会尝试重新发包,当达到重试的时间限制时,将产生系统错误。在我们公司,重新发包的超时时间被设置为 15 分钟。
至此,我已经对 JDBC 的内部操作做了讲解,希望能够让大家学会如何正确的配置超时时间,从而减少错误的发生。
最后,我将列出一些常见的问题。
常见问题(FAQ)
Q1. 我已经使用
Statement.setQueryTimeout()方法设置了查询超时,但在网络出错时并没有产生作用。➔ 查询超时仅在 Socket Timeout 生效的前提下才有效,它并不能用来解决外部的网络错误,要解决这种问题,必须设置 JDBC 的 Socket Timeout。
Q2. Transaction Timeout、Statement Timeout 和 Socket Timeout 和 DBCP 的配置有什么关系?
➔ 当通过 DBCP 获取数据库连接时,除了 DBCP 获取连接时的
waitTimeout配置以外,其他配置对 JDBC 没有什么影响。Q3. 如果设置了 JDBC 的 Socket Timeout,那 DBCP 连接池中处于 IDLE 状态的连接是否也会在达到超时时间后被关闭?
➔ 不会。Socket 的设置只会在产生数据读写时生效,而不会对 DBCP 中的 IDLE 连接产生影响。当 DBCP 中发生新连接创建,老的 IDLE 连接被移除,或是连接有效性校验的时候,Socket 设置会对其产生一定的影响,但除非发生网络问题,否则影响很小。
Q4. Socket Timeout 应该设置为多少?
➔ 就像我在正文中提的那样,Socket Timeout 必须高于 Statement Timeout,但并没有什么推荐值。在发生网络错误的时候,Socket Timeout 将会生效,但是再小心的配置也无法避免网络错误的发生,只是在网络错误发生后缩短服务失效的时间(如果网络恢复正常的话)。
说明:本文部分内容基于较早期的 JDBC 驱动版本(如 MySQL 5.0.8、Lucy 1.5/1.6 等)进行分析,不同数据库驱动的新版本在超时处理机制上可能有所差异,具体配置请以官方文档为准。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/shen-ru-li-jie-jdbc-de-chao-shi-she-zhi.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。