三 代码解读
1. 连接池管理
先看如何获取到数据库连接, 以此为入口。 在 ProxoolDataSource 中:
public Connection getConnection() throws SQLException { ConnectionPool cp = null; try { if (!ConnectionPoolManager.getInstance().isPoolExists(alias)) { registerPool(); } cp = ConnectionPoolManager.getInstance().getConnectionPool(alias); return cp.getConnection(); } catch (ProxoolException e) { LOG.error("Problem gettingconnection", e); throw new SQLException(e.toString()); } }根据 ProxoolDataSource 的 alias 属性先到连接池管理处查询是否已经有对应的连接池,如果没有,则注册相应的连接池, 如果有,则获取Connection 对象。 注册连接池的代码如下:
private synchronized void registerPool()throwsProxoolException { if (!ConnectionPoolManager.getInstance().isPoolExists(alias)) { ConnectionPoolDefinition cpd = newConnectionPoolDefinition(); cpd.setAlias(getAlias()); cpd.setDriver(getDriver()); // cpd.setXXX(getXXX()); // 设置各种属性 // We must setUser and setPassword*after* setDelegateProperties // otherwise the values will beoverwritten. Credit Bulent Erdemir. cpd.setUser(getUser()); cpd.setPassword(getPassword()); ProxoolFacade.registerConnectionPool(cpd); } }
ConnectionPoolManager有一个 connectionPoolMap 对象, 存储从 alias 到 ConnectionPool 的映射。 可见 alias 的重要性。 获取ProxoolDataSource对应的连接池的代码:
protected ConnectionPoolgetConnectionPool(String alias) throws ProxoolException { ConnectionPool cp = (ConnectionPool) connectionPoolMap.get(alias); if (cp == null) { throw newProxoolException(getKnownPools(alias)); } return cp; }
这是创建连接池的代码:
protected ConnectionPoolcreateConnectionPool(ConnectionPoolDefinition connectionPoolDefinition) throwsProxoolException { ConnectionPool connectionPool = newConnectionPool(connectionPoolDefinition); connectionPools.add(connectionPool); connectionPoolMap.put(connectionPoolDefinition.getAlias(),connectionPool); return connectionPool; }
事实上,ConnectionPoolManager 并没有做太多事情, 主要是管理ProxoolDataSource(alias)到相应ConnectionPool的映射关系。ProxoolFacade用于管理连接池的注册、移除及监听事件。注册连接池的主要工作:首先,在 ConnectionPoolManager中创建该连接池并添加映射, 接着启动该连接池的任务, 以及监听器的注册处理程序,最后处理 JDNI 及 JMX 相关。
protected synchronized static voidregisterConnectionPool(ConnectionPoolDefinition connectionPoolDefinition) throwsProxoolException { // check isPoolExists once morenow we are inside synchronized block. if (!ConnectionPoolManager.getInstance().isPoolExists(connectionPoolDefinition.getAlias())){ Properties jndiProperties = extractJndiProperties(connectionPoolDefinition); ConnectionPool connectionPool =ConnectionPoolManager.getInstance().createConnectionPool(connectionPoolDefinition); connectionPool.start(); compositeProxoolListener.onRegistration(connectionPoolDefinition,connectionPoolDefinition.getCompleteInfo()); if (isConfiguredForJMX(connectionPoolDefinition.getCompleteInfo())){ registerForJmx(connectionPoolDefinition.getAlias(),connectionPoolDefinition.getCompleteInfo()); } if (jndiProperties != null) { registerDataSource(connectionPoolDefinition.getAlias(),jndiProperties); } } else { LOG.debug("Ignoring duplicate attemptto register " + connectionPoolDefinition.getAlias()+ "pool"); } }连接池启动任务代码如下:
/** Starts up house keeping andprototyper threads. */ protected void start() throwsProxoolException { connectionPoolUp = true; prototyper = new Prototyper(this); HouseKeeperController.register(this); } /** * Schedule a regular triggerSweep * @param connectionPool identifies the pool */ protected static void register(ConnectionPoolconnectionPool) { String alias =connectionPool.getDefinition().getAlias(); LOG.debug("Registering '" + alias + "' housekeeper"); HouseKeeper houseKeeper = newHouseKeeper(connectionPool); synchronized (LOCK) { houseKeepers.put(alias,houseKeeper); houseKeeperList.add(houseKeeper); if (houseKeeperThreads.size() == 0) { HouseKeeperThread hkt = newHouseKeeperThread("HouseKeeper"); LOG.debug("Starting a house keeperthread"); hkt.start(); houseKeeperThreads.add(hkt); } } }后面再来看这个 houseKeeperThread 的任务内容。 这里暂时搁下。
2. 数据库连接管理
现在来看如何在连接池中获取数据库连接。 在 ConnectionPool 中,获取数据库连接的代码如下:
/** * Get a connection from the pool. If none are available or there was anException * then an exception is thrown andsomething written to the log */ protected ConnectiongetConnection() throws SQLException { String requester = Thread.currentThread().getName(); /* *If we're busy, we need to return asquickly as possible. Because this is unsynchronized * we run the risk of refusing aconnection when we might actually be able to. But that will * only happen when we're right at ornear maximum connections anyway. */ try { prototyper.quickRefuse(); } catch (SQLException e) { connectionsRefusedCount++; if (admin != null) { admin.connectionRefused(); } log.info(displayStatistics() + " -" + MSG_MAX_CONNECTION_COUNT); timeOfLastRefusal = System.currentTimeMillis(); setUpState(StateListenerIF.STATE_OVERLOADED); throw e; } prototyper.checkSimultaneousBuildThrottle(); ProxyConnection proxyConnection = null; try { // We need to look at all theconnections, but we don't want to keep looping round forever for (int connectionsTried = 0;connectionsTried < proxyConnections.size(); connectionsTried++) { // By doing this in a try/catch weavoid needing to synch on the size(). Weneed to do be // able to cope with connectionsbeing removed whilst we are going round this loop try { proxyConnection = (ProxyConnection)proxyConnections.get(nextAvailableConnection); } catch (ArrayIndexOutOfBoundsException e){ // This is thrown by a Vector(which we no longer use), but is // kept here for a while. nextAvailableConnection = 0; proxyConnection =(ProxyConnection) proxyConnections.get(nextAvailableConnection); } catch (IndexOutOfBoundsException e) { // This is thrown by a true List nextAvailableConnection = 0; proxyConnection =(ProxyConnection) proxyConnections.get(nextAvailableConnection); } // setActive() returns false ifthe ProxyConnection wasn't available. You // can't set it active twice (atleast, not without making it available again // in between) if (proxyConnection != null &&proxyConnection.setStatus(ProxyConnectionIF.STATUS_AVAILABLE,ProxyConnectionIF.STATUS_ACTIVE)) { // Okay. So we have it. But is itworking ok? if (getDefinition().isTestBeforeUse()){ if(!testConnection(proxyConnection)) { // Oops. No it's not. Let's choose another. proxyConnection = null; } } if (proxyConnection != null) { nextAvailableConnection++; break; } } else { proxyConnection = null; } nextAvailableConnection++; } // Did we get one? if (proxyConnection == null) { try { // No! Let's see if we can create one proxyConnection = prototyper.buildConnection(ProxyConnection.STATUS_ACTIVE, "ondemand"); // Okay. So we have it. But is itworking ok? if (getDefinition().isTestBeforeUse()){ if(!testConnection(proxyConnection)) { // Oops. Noit's not. There's not much more we can do for now throw new SQLException("Createda new connection but it failed its test"); } } } catch (SQLException e) { throw e; } catch (ProxoolException e) { log.debug("Couldn't getconnection", e); throw new SQLException(e.toString()); } catch (Throwable e) { log.error("Couldn't getconnection", e); throw new SQLException(e.toString()); } } } catch (SQLException e) { throw e; } catch (Throwable t) { log.error("Problem gettingconnection", t); throw new SQLException(t.toString()); } finally { if (proxyConnection != null) { connectionsServedCount++; proxyConnection.setRequester(requester); } else { connectionsRefusedCount++; if (admin != null) { admin.connectionRefused(); } timeOfLastRefusal = System.currentTimeMillis(); setUpState(StateListenerIF.STATE_OVERLOADED); } } if (proxyConnection == null) { throw new SQLException("Unknownreason for not getting connection. Sorry."); } if (log.isDebugEnabled() &&getDefinition().isVerbose()) { log.debug(displayStatistics() + " -Connection #" + proxyConnection.getId() + " served"); } // This gives the proxy connectiona chance to reset itself before it is served. proxyConnection.open(); return ProxyFactory.getWrappedConnection(proxyConnection); }注释写得很详细了, 我就不啰嗦了。
3. 扫描任务
连接池的实现实际上就是基于状态的管理, 状态主要包括: 连接池当前连接数以及连接池配置的那些属性,比如可用空闲连接、最大连接数、最小连接数、连接的最大活跃时长等。 当然, 还涉及比较复杂的并发处理。
扫描任务由HouseKeeper(HouseKeeperThread, HouseKeeperController) 和Prototyper (PrototyperThread, PrototyperController) 来完成。 其中, HouseKeeper 任务在注册连接池时触发, 而 Prototyper 任务由 HouseKeeper 任务触发。
在注册连接池并启动其任务时, HouseKeeperThead 由 HouseKeeperController创建出来并从中获取可用 HouseKeeper 并启动其任务, 其代码如下:
public void run() { while (!stop) { HouseKeeper hk = HouseKeeperController.getHouseKeeperToRun(); while (hk != null && !stop) { try { // if (LOG.isDebugEnabled()) { // LOG.debug("Aboutto sweep " + hk.getAlias()); // } hk.sweep(); } catch (ProxoolException e) { LOG.error("Couldn't sweep " + hk.getAlias(), e); } hk = HouseKeeperController.getHouseKeeperToRun(); } try { Thread.sleep(5000); } catch (InterruptedException e) { LOG.error("Interrupted", e); } } }
HouseKeeper 的任务内容如下:
首先, 获取该连接池的所有连接:
1. 若处于未用状态, 则进行 testsql 测试;
2. 若存在时长超过所设置的最大连接时长, 则进行关闭回收;
3. 若处于活跃状态, 检查其是否超过最大活跃时长, 若超过则回收;
protected void sweep() throws ProxoolException { ConnectionPoolDefinitionIF definition = connectionPool.getDefinition(); Log log = connectionPool.getLog(); Statement testStatement = null; try { connectionPool.acquirePrimaryReadLock(); // Right, now we know we're theright thread then we can carry on house keeping Connection connection = null; ProxyConnectionIF proxyConnection = null; int recentlyStartedActiveConnectionCountTemp= 0; // sanity check int[] verifiedConnectionCountByState = new int[4]; ProxyConnectionIF[] proxyConnections= connectionPool.getProxyConnections(); for (int i = 0; i < proxyConnections.length; i++) { proxyConnection =proxyConnections[i]; connection =proxyConnection.getConnection(); if (!connectionPool.isConnectionPoolUp()) { break; } // First lets check whether theconnection still works. We should only validate // connections that are not isuse! SetOffline only succeeds if theconnection // is available. if (proxyConnection.setStatus(ProxyConnectionIF.STATUS_AVAILABLE, ProxyConnectionIF.STATUS_OFFLINE)) { try { testStatement =connection.createStatement(); // Some DBs return an object evenif DB is shut down if (proxyConnection.isReallyClosed()){ proxyConnection.setStatus(ProxyConnectionIF.STATUS_OFFLINE, ProxyConnectionIF.STATUS_NULL); connectionPool.removeProxyConnection(proxyConnection,ConnectionListenerIF.HOUSE_KEEPER_TEST_FAIL, "it appears to beclosed",ConnectionPool.FORCE_EXPIRY, true); } String sql =definition.getHouseKeepingTestSql(); if (sql != null && sql.length() > 0) { // A Test Statement has beenprovided. Execute it! boolean testResult = false; try { testResult =testStatement.execute(sql); } finally { if (log.isDebugEnabled() &&definition.isVerbose()) { log.debug(connectionPool.displayStatistics() + " - Testing connection " + proxyConnection.getId() +(testResult ? ":True" : ": False")); } } } proxyConnection.setStatus(ProxyConnectionIF.STATUS_OFFLINE, ProxyConnectionIF.STATUS_AVAILABLE); } catch (Throwable e) { // There is a problem with thisconnection. Let's remove it! proxyConnection.setStatus(ProxyConnectionIF.STATUS_OFFLINE, ProxyConnectionIF.STATUS_NULL); connectionPool.removeProxyConnection(proxyConnection,ConnectionListenerIF.HOUSE_KEEPER_TEST_FAIL, "it has problems: " + e, ConnectionPool.REQUEST_EXPIRY, true); } finally { try { testStatement.close(); } catch (Throwable t) { // Never mind. } } } // END if(poolableConnection.setOffline()) // Now to check whether theconnection is due for expiry if (proxyConnection.getAge() >definition.getMaximumConnectionLifetime()) { final String reason = "age is " + proxyConnection.getAge() + "ms"; // Check whether we can make itoffline if(proxyConnection.setStatus(ProxyConnectionIF.STATUS_AVAILABLE, ProxyConnectionIF.STATUS_OFFLINE)) { if(proxyConnection.setStatus(ProxyConnectionIF.STATUS_OFFLINE, ProxyConnectionIF.STATUS_NULL)) { // It is. Expire it now . connectionPool.expireProxyConnection(proxyConnection,ConnectionListenerIF.MAXIMUM_CONNECTION_LIFETIME_EXCEEDED, reason, ConnectionPool.REQUEST_EXPIRY); } } else { // Oh no, it's in use. Never mind, we'll mark it for expiry // next time it is available. This will happen in the // putConnection() method. proxyConnection.markForExpiry(reason); if (log.isDebugEnabled()) { log.debug(connectionPool.displayStatistics() + " - #" + FormatHelper.formatMediumNumber(proxyConnection.getId()) + " marked for expiry."); } } // END if(poolableConnection.setOffline()) } // END if(poolableConnection.getAge() > maximumConnectionLifetime) // Now let's see if thisconnection has been active for a // suspiciously long time. if (proxyConnection.isActive()) { long activeTime = System.currentTimeMillis()- proxyConnection.getTimeLastStartActive(); if (activeTime <definition.getRecentlyStartedThreshold()) { // This connection hasn't beenactive for all that long // after all. And as long as wehave at least one // connection that is"actively active" then we don't // consider the pool to be down. recentlyStartedActiveConnectionCountTemp++; } if (activeTime >definition.getMaximumActiveTime()) { // This connection has been activefor way too long. We're // going to kill it :) connectionPool.removeProxyConnection(proxyConnection,ConnectionListenerIF.MAXIMUM_ACTIVE_TIME_EXPIRED, "it has been active for toolong",ConnectionPool.FORCE_EXPIRY, true); String lastSqlCallMsg; if (proxyConnection.getLastSqlCall()!= null) { lastSqlCallMsg = ", and the last SQL itperformed is '" + proxyConnection.getLastSqlCall() + "'."; } else if(!proxyConnection.getDefinition().isTrace()) { lastSqlCallMsg = ", but the last SQL itperformed is unknown because the trace property is not enabled."; } else { lastSqlCallMsg = ", but the last SQL itperformed is unknown."; } log.warn("#" + FormatHelper.formatMediumNumber(proxyConnection.getId())+ " wasactive for " + activeTime + " milliseconds and has beenremoved automaticaly. The Thread responsible was named '" +proxyConnection.getRequester() + "'" + lastSqlCallMsg); } } // What have we got? verifiedConnectionCountByState[proxyConnection.getStatus()]++; } calculateUpState(recentlyStartedActiveConnectionCountTemp); } catch (Throwable e) { // We don't want the housekeepingthread to fall over! log.error("Housekeeping log.error(:", e); } finally { connectionPool.releasePrimaryReadLock(); timeLastSwept = System.currentTimeMillis(); if (definition.isVerbose()) { if (log.isDebugEnabled()) { log.debug(connectionPool.displayStatistics() + " - House keepingtriggerSweep done"); } } } PrototyperController.triggerSweep(definition.getAlias()); }
从最后一行代码可以看到, HouseKeeper 任务在即将结束时将触发 Prototyper 任务。 首先由 PrototyperController 创建 PrototyperThread, 然后启动其任务。该任务将启动所有ConnectionPool 的 Prototyper 任务。
/** * Trigger prototyping immediately. Runsinside a new Thread so * control returns as quick as possible.You should call this whenever * you suspect that building moreconnections might be a good idea. * @param alias */ protected static void triggerSweep(String alias) { try { // Ensure that we're not in theprocess of shutting down the pool ConnectionPool cp =ConnectionPoolManager.getInstance().getConnectionPool(alias); try { cp.acquirePrimaryReadLock(); // 这里只设置Sweep状态为true, Sweep 动作在 PrototyperThread 统一做 cp.getPrototyper().triggerSweep(); } catch (InterruptedException e) { LOG.error("Couldn't acquire primaryread lock", e); } finally { cp.releasePrimaryReadLock(); } } catch (ProxoolException e) { if (LOG.isDebugEnabled()) { LOG.debug("Couldn't trigger prototypertriggerSweep for '" + alias + "' - maybeit's just been shutdown"); } } startPrototyper(); try { // If we are currently sweepingthis will cause it to loop through // once more keepSweeping = true; // If we aren't already startedthen this will start a new sweep if (prototyperThread != null) { prototyperThread.doNotify(); } } catch (IllegalMonitorStateException e) { LOG.debug("Hmm", e); if (Thread.activeCount() >10 && LOG.isInfoEnabled()){ LOG.info("Suspicious thread count of" + Thread.activeCount()); } } catch (IllegalThreadStateException e) { // Totally expected. Should happenall the time. Just means that // we are already sweeping. if (LOG.isDebugEnabled()) { LOG.debug("Ignoring attempt toprototype whilst already prototyping"); } } } private static void startPrototyper() { if (prototyperThread == null) { synchronized(LOCK) { if (prototyperThread == null) { prototyperThread = new PrototyperThread("Prototyper"); prototyperThread.start(); } } } } public void run() { while (!stop) { int sweptCount = 0; while (PrototyperController.isKeepSweeping()&& !stop) { PrototyperController.sweepStarted(); ConnectionPool[] cps =ConnectionPoolManager.getInstance().getConnectionPools(); for (int i = 0; i < cps.length && !stop; i++) { Prototyper p =cps[i].getPrototyper(); try { cps[i].acquirePrimaryReadLock(); if (cps[i].isConnectionPoolUp()&& p.isSweepNeeded()) { p.sweep(); sweptCount++; } } catch (InterruptedException e) { LOG.error("Couldn't acquire primaryread lock", e); } finally { cps[i].releasePrimaryReadLock(); } } } // if (LOG.isDebugEnabled()) { // LOG.debug("Swept " +sweptCount + " pools"); // } doWait(); } }
Prototyper 负责定时扫描对应的 ConnectionPool,若当前连接数小于指定的最小连接数或可用空闲连接数时创建新的数据库连接,主要有 sweep 和 buildConnection 方法, 代码如下:
/** * Trigger prototyping immediately * @return true if something was prototyped */ protected boolean sweep() { boolean somethingDone = false; try { while (!cancel && connectionPool.isConnectionPoolUp()) { // if (log.isDebugEnabled()) { // log.debug("Prototyping"); // } String reason = null; if (connectionCount >=getDefinition().getMaximumConnectionCount()) { // We don't want to make any morethat the maximum break; } else if (connectionCount < getDefinition().getMinimumConnectionCount()){ reason = "to achieve minimum of " +getDefinition().getMinimumConnectionCount(); } else if (connectionPool.getAvailableConnectionCount() <getDefinition().getPrototypeCount()) { reason = "to keep " +getDefinition().getPrototypeCount() + " available"; } else { // Nothing to do break; } ProxyConnectionIFfreshlyBuiltProxyConnection = null; try { // If it has been shutdown then weshould just stop now. if (!connectionPool.isConnectionPoolUp()) { break; } freshlyBuiltProxyConnection= buildConnection(ConnectionInfoIF.STATUS_AVAILABLE, reason); somethingDone = true; } catch (Throwable e) { log.error("Prototype", e); // If there's been an exception,perhaps we should stop // prototyping for a while. Otherwise if the database // has problems we end up tryingthe connection every 2ms // or so and then the log growspretty fast. break; // Don't wory, we'll start againthe next time the // housekeeping thread runs. } if (freshlyBuiltProxyConnection == null) { // That's strange. No double thebuildConnection() method logged the // error, but we should have builda connection here. } } } catch (Throwable t) { log.error("Unexpected error", t); } return somethingDone; } protectedProxyConnection buildConnection(int status, String creator) throws SQLException,ProxoolException { long id = 0; synchronized (lock) { // Check that we are allowed tomake another connection if (connectionCount >=getDefinition().getMaximumConnectionCount()) { throw new ProxoolException("ConnectionCountis " + connectionCount + ". Maximum connection count of " + getDefinition().getMaximumConnectionCount()+ " cannotbe exceeded."); } checkSimultaneousBuildThrottle(); connectionsBeingMade++; connectionCount++; id = nextConnectionId++; } ProxyConnection proxyConnection = null; Connection realConnection = null; try { // get a new *real* connection final ConnectionPoolDefinition definition= connectionPool.getDefinition(); realConnection = connectionBuilder.buildConnection(definition); // build a proxy around it //TODO BRE: the connection builder hasmade a new connection using the information // supplied in the ConnectionPoolDefinition.That's where it got the URL... // The ProxyConnection is passedthe ConnectionPoolDefinition as well so it doesn't // need the url in itsconstructor... String url = definition.getUrl(); proxyConnection = newProxyConnection(realConnection, id, url, connectionPool, definition,status); try { connectionPool.onBirth(realConnection); } catch (Exception e) { log.error("Problem during onBirth(ignored)", e); } //TODO BRE: the actual pool ofconnections is maintained by the ConnectionPool. I'm not // very happy with the idea ofletting the Prototyper add the newly build connection // into the pool itself. It shouldrather be the pool that does it, after it got a new // connection from the Prototyper.This would clearly separate the responsibilities: // ConnectionPool maintains thepool and its integrity, the Prototyper creates new // connections when instructed. boolean added = connectionPool.addProxyConnection(proxyConnection); if (log.isDebugEnabled()) { // 打印日志 } if (!added) { proxyConnection.reallyClose(); } } catch (SQLException e) { // log.error(displayStatistics() +" - Couldn't initialise connection #" + proxyConnection.getId() +": " + e); throw e; } catch (RuntimeException e) { if (log.isDebugEnabled()) { log.debug("Prototyping problem", e); } throw e; } catch (Throwable t) { if (log.isDebugEnabled()) { log.debug("Prototyping problem", t); } throw new ProxoolException("Unexpectedprototyping problem", t); } finally { synchronized (lock) { if (proxyConnection == null) { // If there has been an exceptionthen we won't be using this one and // we need to decrement thecounter connectionCount--; } connectionsBeingMade--; } } return proxyConnection; }DefaultConnectionBuilder负责创建不带连接池属性的“真实数据库连接”,代码如下:
public ConnectionbuildConnection(ConnectionPoolDefinitionIF cpd) throws SQLException { Connection realConnection = null; final String url = cpd.getUrl(); Properties info =cpd.getDelegateProperties(); return DriverManager.getConnection(url,info); }至此, Proxool 连接池实现的主要流程代码讲解完毕。留下来的就是一些状态、并发处理的细节。这还需要更深入去学习,至少并发处理就是首先要跨越的门槛。