才工作一两年用Oracle的时候,一些网络问题会造成应用线程一直卡在 SocketInputStream.read()
上,或者卡了某个时间段后报出java.sql.SQLRecoverableException: IO 错误: Socket read timed out
。 当时周围没人知道这个问题的原因,我在排查过程中发现,大家都百度的时候,谁能Google反而往往更能找到问题的真正原因。。。最终在http://www-01.ibm.com/support/docview.wss?uid=swg1PM91941 里找到了答案,设置超时参数oracle.jdbc.ReadTimeout
,继续搜索又碰到了极品好文 Understanding JDBC Internals & Timeout Configuration。
当时因为只要有SocketTimeoutException发生,再调用Connection对象的一些方法就会报错,所以下意识以为类似socketTimeout的参数超时会直接导致socket不可用。在我弄分库分表中间之后,需要了解mysql通信协议,测试的时候发现,Socket read timed out
后socket仍然可用,并且jdbc驱动会发送数据库协议层的挥手请求,正常关闭连接!(其实人家javadoc就是这么写的啊)
Connection层面关于socket的两个超时参数
- connect timeout
连接到server的超时时间,对应java.net.Socket#connect(java.net.SocketAddress, int)的第二个参数
public void connect(SocketAddress endpoint, int timeout) throws IOException
- socket timeout
等待读取数据的超时时间,传递给java.net.Socket#setSoTimeout方法
public synchronized void setSoTimeout(int timeout) throws SocketException
java.net.Socket#setSoTimeout的javadoc
Enable/disable SO_TIMEOUT with the specified timeout, in milliseconds. With this option set to a non-zero timeout, a read() call on the InputStream associated with this Socket will block for only this amount of time. If the timeout expires, a java.net.SocketTimeoutException is raised, though the Socket is still valid. The option must be enabled prior to entering the blocking operation to have effect. The timeout must be > 0. A timeout of zero is interpreted as an infinite timeout.
javadoc清楚的写着,超时以后java.net.SocketTimeoutException会被抛出,但是Socket还是可用的。
在mysql-jdbc驱动里面,SocketTimeoutException被包装成CommunicationsException向上抛出,连接关闭清理资源是会调用com.mysql.jdbc.MysqlIO#quit,这里会用当前socket继续向mysql发送QUIT包。如果客户端连接的是分库分表中间件的话,QUIT包确保了中间件那里不会有类似java.io.IOException: 远程主机强迫关闭了一个现有的连接
这样的报错出现。
"main@1" prio=5 tid=0x1 nid=NA runnable
java.lang.Thread.State: RUNNABLE
at com.mysql.jdbc.MysqlIO.quit(MysqlIO.java:2261)
at com.mysql.jdbc.ConnectionImpl.realClose(ConnectionImpl.java:4232)
at com.mysql.jdbc.ConnectionImpl.cleanup(ConnectionImpl.java:1338)
at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2517)
- locked <0x3cf> (a com.mysql.jdbc.JDBC4Connection)
at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2449)
at com.mysql.jdbc.StatementImpl.executeQuery(StatementImpl.java:1381)
at com.tankilo.App.main(App.java:26)
java.net.Socket#setSoTimeout可以被多次调用
查看mysql jdbc驱动源码可以看出,驱动自己执行某些内置SQL时,用户的socketTimeout参数很大程度不合适,所以执行前会把用户设置的socketTimeout参数暂存,然后调用java.net.Socket#setSoTimeout临时设置合理的超时参数,在执行完内置SQL后再通过java.net.Socket#setSoTimeout将用户设置的socketTimeout还原。
用户如何临时调整socketTimeout参数
在mysql-jdbc驱动里,上面两个超时参数可以在获取连接时,通过【jdbc url(例如jdbc:mysql://localhost:3306/db1?connectTimeout=60000&socketTimeout=60000
)】或者【java.sql.DriverManager#getConnection里的Properties对象】去设置。但是如果用户想在连接建立后,像驱动源码里一样临时为某个sql调整socketTimeout应该怎么办,侵入到驱动实现里获取socket对象?
不需要,JDBC驱动接口类已经提供了对应的API.
// java.sql.Connection#setNetworkTimeout
void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException;
这个方法的javadoc也是非常清楚的,
Sets the maximum period a Connection or objects created from the Connection will wait for the database to reply to any one request. If any request remains unanswered, the waiting method will return with a SQLException, and the Connection or objects created from the Connection will be marked as closed. Any subsequent use of the objects, with the exception of the close, isClosed or Connection.isValid methods, will result in a SQLException.
Note: This method is intended to address a rare but serious condition where network partitions can cause threads issuing JDBC calls to hang uninterruptedly in socket reads, until the OS TCP-TIMEOUT (typically 10 minutes).This method can be invoked more than once, such as to set a limit for an area of JDBC code, and to reset to the default on exit from this area. Invocation of this method has no impact on already outstanding requests.
When the driver determines that the setNetworkTimeout timeout value has expired, the JDBC driver marks the connection closed and releases any resources held by the connection.
这个方法的问题
java.sql.Connection#setNetworkTimeout需要两个参数,一个java.util.concurrent.Executor对象,一个超时参数值。mysql-jdbc驱动会在用java.util.concurrent.Executor运行com.mysql.jdbc.ConnectionImpl.NetworkTimeoutSetter
类,最终会去调用java.net.Socket#setSoTimeout
。
如果第一个参数传入线程池的话,这个过程是个异步过程,而java.sql.Connection#setNetworkTimeout本身也没返回像java.util.concurrent.Future
这样的操作结果占位符。如果你调用完java.sql.Connection#setNetworkTimeout
立马执行SQL,新的超时参数可能不会在这条SQL上生效,因为你的SQL和执行NetworkTimeoutSetter的异步线程在并发执行。
保险起见应该这样设置? 在当前线程直接执行。
conn.setNetworkTimeout(new Executor() {
@Override public void execute(Runnable command) {
command.run();
}
}, 4000);
总结
本文只是一些比较零碎的个人发现,系统的介绍jdbc超时参数的文章,开头也提到过。只是在学习分库分表中间的过程中,对这些参数有了自我实践的认识。回想以前查oracle jdbc驱动的时候,大家都是百度,凭什么别人百度到,你百度不到?(别人能看Oracle付费知识库Orz...)。现在用mysql,最大的好处是开源网上信息很多,自己也可以看源码,即使老同事口口相传,你不问,他不说,你也可以自己去从代码层级确定某些问题。开放,大家一起讨论,其乐无穷。