深入理解JDBC的超时设置
这是最近读到的讲关于 JDBC 的超时问题最透彻的文章,原文是http://www.cubrid.org/blog/understanding-jdbc-internals-and-timeout-configuration ,网上现有的翻译感觉磕磕绊绊的,很多上下文信息丢失了,这里用我的理解重新翻译一下。
应用程序中配置恰当的 JDBC 超时时间能减少服务失败的时间,这篇文章我们将讨论不同种类的超时和推荐的配置。
Web 应用服务器在 DDoS 攻击后变得无响应
(这是一个真实案例的发生过程复述)
在 DDoS 攻击之后,整个服务都不能正常工作了,因为第四层交换机不能工作,网络连接断开了,这也导致 WAS (可以将 WAS 理解为作者公司的应用程序)不能正常工作。攻击发生后不久,安全团队拦截了所有 DDoS 攻击,然后网络恢复正常,但 WAS 还是不能工作。
通过分析系统的 dump 日志发现,业务系统停在了 JDBC API 的调用上。20分钟后系统仍处于等待状态无法响应,大概过了30分钟,系统突然发生异常,然后服务恢复正常。
为什么已经将查询超时时间设置成3秒, WAS 却等待了30分钟?为什么30分钟后 WAS 又开始工作了?
如果理解了 JDBC 的超时机制就能找到答案。
为什么我们需要知道 JDBC 驱动
当有性能问题或系统级错误时,WAS 和数据库是我们关注的两个重要层面。在我公司 WAS 和数据库通常由不同的部门负责,因此每个部门聚焦在各自负责的领域来设法弄清楚状况。此时 WAS 和数据库之间的部分会因为得不到足够的关注而产生盲区。对于 Java 应用,这个盲区在数据库连接池和 JDBC 之间,本文我们将重点讨论 JDBC。
什么是 JDBC 驱动
JDBC 是 Java 应用程序中用于访问数据库的一套标准 API,Sun 公司定义了4种类型的 JDBC 驱动。我公司主要用的是第4种,该类型驱动由纯 Java 语言编写,在 Java 应用中通过 socket 与数据库通信。
类型4驱动是通过 socket 来处理字节流的,它的基本操作和 HttpClient 这种网络操作类库相同。同其他网络类库一样,也会在发生超时的时候占用大量的 CPU 资源从而失去响应。如果你之前用过 HttpClient ,肯定遇到过因为没有设置超时导致的错误。如果 socket 超时设置不合适,类型4驱动也可能有同样的错误(连接被阻塞)。
下面让我们了解如何配置 JDBC 驱动的 socket 超时,以及设置时需考虑哪些问题。
WAS 与数据库间的设置超时的层次
图2展示了简化的 WAS 和数据库通信时的超时层次。
更上层的超时依赖于下层的超时,只有当较低层的超时机制正常工作,上层的超时才会正常。如果 JDBC 驱动程序的 socket 超时工作不正常,那么更上层的超时比如 Statement 超时和事务超时都不会正常工作。
我们收到很多评论说:
即使配置了 Statement 超时,应用程序还是不能从故障中恢复,因为 Statement 超时在网络故障时不起作用。
Statement 超时在网络故障时不起作用。它只能做到:限制一次Statement 执行的时间,处理超时以防网络故障必须由 JDBC 驱动来做。
JDBC 驱动的 socket 超时还会受操作系统的 socket 超时配置的影响。这解释了为什么案例中的 JDBC 连接在网络故障后阻塞了30分钟才恢复,即使没配置 JDBC 驱动的 socket 超时。
DBCP 连接池位于图2的左边。你会发现各种层面的超时与 DBCP 是分开的。DBCP 负责数据库连接(即本文中说到的Connection)的创建和管理,并不涉及超时的处理。当在 DBCP 中创建了一个数据库连接或发送了一条查询校验的 sql 语句用于检查连接有效性时,socket 超时会影响这些过程的处理,但并不直接影响应用程序。
然而在应用程序中调用 DBCP 的 getConnection() 方法时,你能指定应用程序获取数据库连接的超时时间,但这和 JDBC 的连接超时无关。
什么是事务超时
事务超时是在框架(Spring、EJB容器)或应用程序层面上才有效的超时。
事务超时可能是个不常见的概念。简单讲,事务超时等于** Statement 超时 * N(需要执行的 Statement 的数量) + 其它(垃圾回收等其他时间)**。事务超时被用来限制执行一个事务之内所有 Statement 执行的总时长。
比如,假设执行一次 Statement 执行需0.1秒,那执行几次 Statement
并不是什么问题,但如果是执行十万次则需要一万秒(大约7个小时),这就可以用上事务超时了。
EJB 的声明式事务管理 (容器管理事务) 就是一种典型的使用场景,但声明式事务管理只是定义了相应的规范,容器内事务的处理过程和具体实现由容器的开发者负责。我们公司并没有用 EJB,用的是最常见的 Spring 框架,所以事务超时的配置也由 Spring 来管理。在 Spring 中,事务超时可以在 XML 文件显式配置或在 Java 代码中用 Transactional 注解来配置。
<tx:attributes>
<tx:method name="…" timeout="3"/>
tx:attributes>
Spring 提供的事务超时的配置非常简单,它会记录每个事务的开始时间和消耗时间,当特定的事件发生时会对已消耗掉的时间做校验,如果超出了配置将抛出异常。
Spring 中数据库连接被保存在线程本地变量(ThreadLocal)中,这被称作事务同步(Transaction Synchronization)。当数据库连接被保存到 ThreadLocal 时,同时会记录事务的开始时间和超时时间。所以通过数据库连接的代理创建的 Statement 在执行时就会校验这个时间。
EJB 的声明式事务管理的实现也是类似,实现的思路非常简单。如果事务超时非常重要,但你所使用的容器或框架不提供此功能,你也可以选择自己实现,关于事务超时并没有制定标准的 API。
Lucy 框架的1.5和1.6版不支持事务超时,但你可以通过 Spring 的事务管理达到相同的效果。
假设一个事务里有5条 Statement ,每条 Statement 执行时间是200毫秒,其它业务逻辑或框架操作的执行时间是100毫秒,那事务允许的超时时间至少应该1100毫秒(200 * 5 + 100)。
什么是 Statement 超时
Statement 超时是用来限制 Statement 的执行时间的,它的具体值是通过 JDBC API 来设置的。JDBC 驱动程序基于这个值进行 Statement 执行时的超时处理。Statement 超时是通过 JDBC API 中java.sql.Statement 类的 setQueryTimeout(int timeout) 方法配置的。不过现在的开发者已经很少直接在代码中配置它了,更多是通过框架来进行设置。
以 iBatis 为例,可以通过 SqlMapConfig.xml 中的 setting 属性defaultStatementTimeout 来设置全局的 statement 超时缺省值。你也可以通过在具体的 sql 映射文件中的 select insert update 标签的 statement 属性来覆盖。
当你用 Lucy 1.5或1.6版时,可以通过设置 queryTimeout 属性在数据源层面设置 Statement 超时。
Statement 超时的具体数值需要根据每个应用自身的情况而定,并没有推荐的配置。
JDBC 驱动中的 Statement 超时处理过程
每个数据库和驱动程序的 Statement 超时的处理也是不同的。Oracle 和 SQLServer 的工作方式比较像,MySQL 和 CUBRID 比较像。
Oracle 中的 Statement 超时处理
- 调用 Connection 的 createStatement() 方法创建一个 Statement 对象
- 调用 Statement 的 executeQuery() 方法
- Statement 通过内部绑定的 Connection 对象将查询命令发送到 Oracle 数据库
- Statement 向 Oracle 的超时处理线程 OracleTimeoutPollingThread(每个类加载器一个该线程)注册一个 Statement 用于处理超时
- 发生超时
- Oracle 的 OracleTimeoutPollingThread 调用 OracleStatement 的 cancel() 方法
- 通过 Statement 的 Connection 发送一条消息取消还在执行的查询
JTDS (MS SQLServer) 中的 Statement 超时处理
1.调用 Connection 的 createStatement() 方法创建一个 Statement 对象
- 调用 Statement 的 executeQuery() 方法
- Statement 通过内部的 Connection 将查询命令发送到 MS SqlServer 数据库
- Statement 向 MS SQLServer 的 TimerThread 线程注册一个 Statement 用于处理超时
- 发生超时
- TimerThread 调用 JtdsStatement 内部的 TsdCore.cancel()方法
- 通过 ConnectionJDBC 发送一条消息取消还在执行的查询
MySQL (5.0.8) 中的 Statement 超时处理
- 调用 Connection 的 createStatement() 方法创建一个 Statement 对象
- 调用 Statement 的 executeQuery() 方法
- Statement 通过内部的 Connection 将查询命令传输到 MySqlServer 数据库
- Statement 创建一个新的超时执行线程(timeout-execution)来处理超时
- 5.1以上版本改为每个连接分配一个线程
- 向 timeout-execution 线程注册当前的 Statement
- 发生超时
- timeout-execution 线程创建一个相同配置的 Connection
- 用新创建的 Connection 发送取消查询的命令
CUBRID中的 Statement 超时处理
- 调用 Connection 的 createStatement() 方法创建一个 Statement 对象
- 调用 Statement 的 executeQuery() 方法
- Statement 通过内部的 Connection 将查询命令发送到 CUBRID 数据库
- Statement 创建一个新的超时执行线程(timeout-execution)来处理超时
- 向 timeout-execution 线程注册当前的 Statement
- 发生超时
- timeout-execution 线程创建一个相同配置的Connection
- 用新创建的 Connection 发送取消查询的命令
什么是 Socket 超时
类型4的 JDBC 驱动是用 Socket 方式与数据库连接的,应用程序和数据库之间的连接超时并不是由数据库处理的。
当数据库突然宕掉或发生网络错误(设备故障等)时,JDBC 驱动的 Socket 超时的值是必须的。由于 TCP/IP 的结构,Socket 没有办法检测到网络错误,因此应用不能检测到与数据库到连接断开了。如果没有设置 Socket 超时,应用程序会一直等待数据库返回结果。(这个连接也被叫做“死连接”) 为了避免死连接,Socket 必须要设置超时时间。Socket 超时可以通过 JDBC 驱动程序配置。通过设置 Socket 超时,可以防止出现网络错误时一直等待的情况并缩短故障时间。
不推荐使用 Socket 超时来限制一个 Statement 的执行时间,因此Socket 超时的值必须要高于 Statement 的超时时间,否则 Socket 超时将会先生效,这样 Statement 超时就没有意义,也无法生效。
下面展示了 Socket 超时设置的连个选项,其配置因不同的驱动而异。
- Socket 连接时的超时:通过 Socket 对象的 connect(SocketAddress endpoint, int timeout) 方法来配置
- Socket 读写时的超时:通过 Socket 对象的 setSoTimeout(int timeout) 方法来配置
通过查看CUBRID,MySQL,MS SQL Server (JTDS) 和 Oracle 的JDBC 驱动源码,我们确认以上所有驱动都是使用上面的2个 API 来设置socket 超时的。
下面列出了如何配置 Socket 超时
JDBC 驱动 | 连接超时配置 | Socket 超时配置 | JDBC Url 格式 | 示例 |
---|---|---|---|---|
MySQL | connectTimeout(默认值:0,单位:毫秒) | 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 , jTDS | loginTimeout(默认值:0,单位:秒) | socketTimeout(默认值:0,单位:s) | jdbc:jtds:<server_type>://[:][/][;=[;...]] | jdbc:jtds:sqlserver://server:port/database;loginTimeout=60;socketTimeout=60 |
Oracle | oracle.net.CONNECT_TIMEOUT (默认值:0,单位:毫秒) | oracle.jdbc.ReadTimeout(默认值:0,单位:毫秒) | 不支持通过url配置,只能通过OracleDatasource.setConnectionProperties() API设置,使用DBCP时可以调用BasicDatasource.setConnectionProperties()或BasicDatasource.addConnectionProperties()进行设置 | - |
CUBRID | 无单独配置项(默认值:5,000,单位:毫秒) | 无单独配置项(默认值:5,000,单位:毫秒) | - | - |
- connectTimeout 和 socketTimeout 的默认值是 0 ,这意味着不会发生超时。
- 你也可以通过属性进行配置,而无需直接使用 DBCP 的 API 。
通过属性进行配置时,需要传入的 key 为 "connectionProperties",其 value 的格式为" [propertyName=property;]*"。下面是 iBatis 中通过 xml 文件配置属性的例子。
type="JDBC">
type="com.nhncorp.lucy.db.DbcpDSFactory">
....
name="connectionProperties" value="oracle.net.CONNECT_TIMEOUT=6000;oracle.jdbc.ReadTimeout=6000"/>
>
>
操作系统层面的 Socket 超时配置
如果没设置 Socket 超时或连接超时,应用程序多数情况下无法检测到网络错误。此时,应用程序将一直等待下去,直到连接上数据库或能读取到数据。然而,如果查看实际服务遇到的实际情况会发现问题常常在在应用程序(WAS)在30分钟后尝试重新连接到网络后被解决了。这是因为操作系统也配置了 Socket 超时时间。我公司使用的 Linux 服务器将 Socket 超时时间设置为30分钟。它将在操作系统层面对网络连接做校验。因为公司的 Linux 服务器的 KeepAlive 检查周期为30分钟,因此即使应用程序里将 Socket 超时设置为0,由网络原因引起的数据库网络连接问题也不会超过30分钟。
通常,应用程序会在调用 Socket 的 read() 方法时由于网络问题而阻塞住。然而很少在调用 Socket 的 write() 方法时处于等待状态,这取决于网络构成和错误类型。当应用程序调用 Socket 的 write() 方法时,数据被记录到操作系统的内核缓冲区,然后将控制权立即交还给应用程序。因此,一旦数据已经写入内核缓冲区,write() 的调用始终是成功。但是,如果操作系统内核缓冲区由于特殊的网络错误而满了的话,write() 方法也会进入等待状态。这种情况下,操作系统会尝试重新发送数据包一段时间,并在达到超时限制时产生错误。 在公司的 Linux服务器上这种情况的超时时间设置为15分钟。
至此,我已经解释了 JDBC 的内部操作,希望这将帮助你正确的超时配置超时时间从而减少错误。
至此,我已经对JDBC的内部操作做了讲解,希望能够让大家学会如何正确的配置超时时间,从而减少错误的发生。
最后,我将列出一些常见的问题。
mysql的三种驱动类型_浅谈4种类型的JDBC驱动程序 _MySQL
JDBC
在网上下载JDBC驱动程序,常看见Type4字样,开始以为是厂商自己的标准,近来翻看资料,原来是SUN定义的JDBC标准。
Type1 JDBC-ODBC桥
作为JDK1.1后的一部分,是sun.jdbc.odbc包的一部分
Application--->JDBC-ODBC Bridge(Type1 jdbc driver)---->JDBC-ODBC Library--->ODBC Driver-->Database
适用于快速的原型系统,没有提供JDBC驱动的数据库如Access
Type2 JAVA to Native API
利用开发商提供的本地库来直接与数据库通信。
Application--->JDBC Driver(Type2 jdbc driver)---->Native Database library---->Database
比Type1性能略好。
Type3 Java to net
Application--->Jdbc Driver(Type3 jdbc driver)----->java middleware--->JDBC Driver---->Database
具有最大的灵活性,通常由那些非数据库厂商提供,是四种类型中最小的。
Type4 JAVA to native dababase
Application--->Jdbc driver(type4 jdbc driver)----->database engine--->database
最高的性能,通过自己的本地协议直接与数据库引擎通信,具备在Internet装配的能力。
恰当的JDBC超时设置能够有效地减少服务失效的时间。本文将对数据库的各种超时设置及其设置方法做介绍。
深入理解JDBC的超时设置
在遭到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与数据库进行通信。
Figure 1: JDBC Type 4.
第4种类型的JDBC通过socket对字节流进行处理,因此也会有一些基本网络操作,类似于HttpClient这种用于网络操作的代码库。当在网络操作中遇到问题的时候,将会消耗大量的cpu资源,并且失去响应超时。如果你之前用过HttpClient,那么你一定遇到过未设置timeout造成的错误。同样,第4种类型的JDBC,若没有合理地设置socket timeout,也会有相同的错误——连接被阻塞。
接下来,就让我们来学习一下如何正确地设置socket timeout,以及需要考虑的问题。
应用与数据库间的timeout层级
Figure 2: Timeout Class.
上图展示了简化后应用与数据库间的timeout层级。(译者注:WAS/BLOC是作者公司的具体应用名称,无需深究)
高级别的timeout依赖于低级别的timeout,只有当低级别的timeout无误时,高级别的timeout才能确保正常。例如,当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毫不相关。
Figure 3: Timeout for Each Levels.
什么是Transaction Timeout?
transaction timeout一般存在于框架(Spring, EJB)或应用级。transaction timeout或许是个相对陌生的概念,简单地说,transaction timeout就是“statement Timeout * N(需要执行的statement数量) + @(垃圾回收等其他时间)”。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语句的超时时间进行独立的配置。
如果你使用的是Lucy1.5或1.6版本,通过设置queryTimeout属性可以在datasource层面对statement timeout进行设置。
statement timeout的具体值需要依据应用本身的特性而定,并没有可供推荐的配置。
JDBC的statement timeout处理过程
不同的关系型数据库,以及不同的JDBC驱动,其statement timeout处理过程会有所不同。其中,Oracle和MS SQLServer的处理相类似,MySQL和CUBRID类似。
Oracle JDBC Statement的QueryTimeout处理过程
1. 通过调用Connection的createStatement()方法创建statement
2. 调用Statement的executeQuery()方法
3. statement通过自身connection将query发送给Oracle数据库
4. statement在OracleTimeoutPollingThread(每个classloader一个)上进行注册
5. 达到超时时间
6. OracleTimeoutPollingThread调用OracleStatement的cancel()方法
7. 通过connection向正在执行的query发送cancel消息
Figure 4: Query Timeout Execution Process for Oracle JDBC Statement.
JTDS (MS SQLServer) Statement的QueryTimeout处理过程
1. 通过调用Connection的createStatement()方法创建statement
2. 调用Statement的executeQuery()方法
3. statement通过自身connection将query发送给MS SqlServer数据库
4. statement在TimerThread上进行注册
5. 达到超时时间
6. TimerThread调用JtdsStatement实例中的TsdCore.cancel()方法
7. 通过ConnectionJDBC向正在执行的query发送cancel消息
Figure 5: QueryTimeout Execution Process for JTDS (MS SQLServer) Statement.
MySQL JDBC Statement的QueryTimeout处理过程
1. 通过调用Connection的createStatement()方法创建statement
2. 调用Statement的executeQuery()方法
3. statement通过自身connection将query发送给MySQL数据库
4. statement创建一个新的timeout-execution线程用于超时处理
5. 5.1版本后改为每个connection分配一个timeout-execution线程
6. 向timeout-execution线程进行注册
7. 达到超时时间
6. TimerThread调用JtdsStatement实例中的TsdCore.cancel()方法
7. timeout-execution线程创建一个和statement配置相同的connection
8. 使用新创建的connection向超时query发送cancel query(KILL QUERY "connectionId")
Figure 6: QueryTimeout Execution Process for MySQL JDBC Statement (5.0.8).
UBRID JDBC Statement的QueryTimeout处理过程
1. 通过调用Connection的createStatement()方法创建statement
2. 调用Statement的executeQuery()方法
3. statement通过自身connection将query发送给CUBRID数据库
4. statement创建一个新的timeout-execution线程用于超时处理
5. 5.1版本后改为每个connection分配一个timeout-execution线程
6. 向timeout-execution线程进行注册
7. 达到超时时间
6. TimerThread调用JtdsStatement实例中的TsdCore.cancel()方法
7. timeout-execution线程创建一个和statement配置相同的connection
8. 使用新创建的connection向超时query发送cancel消息
Figure 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能够避免应用在发生网络错误时产生无休止等待的情况,缩短服务失效的时间。
不推荐使用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 | Default | Unit | Application Method |
socketTimeout | Default | Unit | ||
MySQL Driver | connectTimeout | 0 | ms |
Specify the option in the DriverURL. Example:
jdbc:mysql: //xxx.xx.xxx.xxx:3306/database?connectTimeout=60000&socketTimeout=60000 |
socketTimeout | 0 | ms | ||
MS-SQL Driver jTDS Driver |
loginTimeout | 0 | sec |
Specify the option in the DriverURL. Format: Example:
jdbc:jtds:sqlserver: //server:port/database;loginTimeout=60;socketTimeout=60
|
socketTimeout | 0 | sec | ||
Oracle Thin Driver | oracle.net.CONNECT_TIMEOUT | 0 | ms | Not possible with the driverURL. Must be delivered to the properties object via OracleDatasource.setConnectionProperties() API. When DBCP is used, use the following APIs:
|
oracle.jdbc.ReadTimeout | 0 | ms | ||
CUBRID Thin Driver | No separate configuration | 5,000 | ms | Not possible with the driverURL. Timeout occurs in 5 seconds.
|
- 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将会生效,但是再小心的配置也无法避免网络错误的发生,只是在网络错误发生后缩短服务失效的时间(如果网络恢复正常的话)。