[mysql driver]query timeout实现

之前一直对jdbc的query timeout有疑惑,看了下mysql的实现,大概了解了。

关于jdbc的各种超时时间关系见这个文章http://www.importnew.com/2466.html,这里简单来看一下mysql的query timeout实现,也是比较简单。query timeout就是在发起请求前,启动一个定时任务,触发之后抛出异常。

核心代码再执行sql过程中

protected synchronized ResultSetInternalMethods executeInternal(int maxRowsToRetrieve,
			Buffer sendPacket, boolean createStreamingResultSet,
			boolean queryIsSelectOnly, Field[] metadataFromCache,
			boolean isBatch)
			throws SQLException {
		try {
			//恢复超时状态
			resetCancelledState();
			//物理连接
			MySQLConnection locallyScopedConnection = this.connection;
			
			this.numberOfExecutions++;
	
			if (this.doPingInstead) {
				doPingInstead();
				
				return this.results;
			}
			
			ResultSetInternalMethods rs;
			
			CancelTask timeoutTask = null;
	
			try {
				//如果设置了query timeout,则启动一个timer任务
				if (locallyScopedConnection.getEnableQueryTimeouts() &&
						this.timeoutInMillis != 0
						&& locallyScopedConnection.versionMeetsMinimum(5, 0, 0)) {
					timeoutTask = new CancelTask(this);
					locallyScopedConnection.getCancelTimer().schedule(timeoutTask, 
							this.timeoutInMillis);
				}
				//这里执行sql,因为mysql是bio的,所以正常的话线程会等待server端返回,要么就socket read timeout抛出异常
				rs = locallyScopedConnection.execSQL(this, null, maxRowsToRetrieve, sendPacket,
					this.resultSetType, this.resultSetConcurrency,
					createStreamingResultSet, this.currentCatalog,
					metadataFromCache, isBatch);
				//如果需要超时,即使服务端正常返回了,还是会抛出超时异常
				if (timeoutTask != null) {
					//先把timer任务停了,大部分情况下超时任务还没到点触发
					timeoutTask.cancel();
					
					locallyScopedConnection.getCancelTimer().purge();
					//如果在进行超时操作的时候抛出了异常,则直接抛出这个异常
					if (timeoutTask.caughtWhileCancelling != null) {
						throw timeoutTask.caughtWhileCancelling;
					}
					
					timeoutTask = null;
				}
				synchronized (this.cancelTimeoutMutex) {
					if (this.wasCancelled) {
						SQLException cause = null;
						
						//如果被超时处理了,则抛出MySQLTimeoutException
						if (this.wasCancelledByTimeout) {
							cause = new MySQLTimeoutException();
						} 
						//如果是被cancel了,就抛出cancel异常						
						else {
							cause = new MySQLStatementCancelledException();
						}
						//完了之后,重新恢复状态,等待下一次sql请求
						resetCancelledState();
						//抛出异常
						throw cause;
					}
				}
			} finally {
				//最后的时候,取消超时任务,大部分场景都是正常访问,timer没到点就返回了
				if (timeoutTask != null) {
					timeoutTask.cancel();
					locallyScopedConnection.getCancelTimer().purge();
				}
			}
			
			return rs;
		} catch (NullPointerException npe) {
			checkClosed(); // we can't synchronize ourselves against async connection-close
			               // due to deadlock issues, so this is the next best thing for
			 			   // this particular corner case.
			
			throw npe;
		}
	}

 具体的超时实现CancelTask

public void run() {
					//超时的时候,杀连接
					if (connection.getQueryTimeoutKillsConnection()) {
						try {
							toCancel.wasCancelled = true;
							toCancel.wasCancelledByTimeout = true;
							connection.realClose(false, false, true, 
									new MySQLStatementCancelledException(Messages.getString("Statement.ConnectionKilledDueToTimeout")));
						} catch (NullPointerException npe) {
							// not worth guarding against
						} catch (SQLException sqlEx) {
							caughtWhileCancelling = sqlEx;
						}
					} else {
						//正常的关闭当前sql请求
						Connection cancelConn = null;
						java.sql.Statement cancelStmt = null;
	
						try {
							synchronized (cancelTimeoutMutex) {
								//复制一个连接,这里会重新创建新的连接,如果此时db down了,则会connect timeout。然后发送kill指令,如果db down了,发送不成功,抛出异常,此时cancel失败
								if (origConnURL == connection.getURL()) {
									//All's fine
									cancelConn = connection.duplicate();
									cancelStmt = cancelConn.createStatement();
									cancelStmt.execute("KILL QUERY " + connectionId);
								} else {
									try {
										cancelConn = (Connection) DriverManager.getConnection(origConnURL, origConnProps);
										cancelStmt = cancelConn.createStatement();
										cancelStmt.execute("KILL QUERY " + connectionId);
									} catch (NullPointerException npe){
										//Log this? "Failed to connect to " + origConnURL + " and KILL query"
									}
								}
								//设置状态,方便主线程进行超时判断
								toCancel.wasCancelled = true;
								toCancel.wasCancelledByTimeout = true;
							}
						}
						//如果超时任务抛出异常,比如db挂了,则给主线程一个异常						
						 catch (SQLException sqlEx) {
							caughtWhileCancelling = sqlEx;
						} catch (NullPointerException npe) {
							// Case when connection closed while starting to cancel
							// We can't easily synchronize this, because then one thread
							// can't cancel() a running query
	
							// ignore, we shouldn't re-throw this, because the connection's
							// already closed, so the statement has been timed out.
						} finally {
							if (cancelStmt != null) {
								try {
									cancelStmt.close();
								} catch (SQLException sqlEx) {
									throw new RuntimeException(sqlEx.toString());
								}
							}
	
							if (cancelConn != null) {
								try {
									cancelConn.close();
								} catch (SQLException sqlEx) {
									throw new RuntimeException(sqlEx.toString());
								}
							}
							
							toCancel = null;
							origConnProps = null;
							origConnURL = null;
						}
					}
				}

从上可知,如果发送sql请求之后db挂了,query timeout触发时还没恢复,则query timeout不成功,此时主线程还会等待server端返回直到socket read timeout抛出异常,而如果socket read timeout设置比query timeout小,则query timeout始终无效,因为超时任务还没启动,主线程就抛出socket timeout异常了。

猜你喜欢

转载自iwinit.iteye.com/blog/1933399