歡迎關注我的公衆號:Java技術大雜燴,天天10點精美文章準時奉上java
相關文章sql
前言數據庫
類圖緩存
工廠類實現安全
數據庫鏈接實現服務器
鏈接池的實現ide
從鏈接池中獲取鏈接(流程圖)源碼分析
把鏈接放入到鏈接池中(流程圖)測試
在使用 Mybatis 的時候,數據庫的鏈接通常都會使用第三方的數據源組件,如 C3P0,DBCP 和 Druid 等,其實 Mybatis 也有本身的數據源實現,能夠鏈接數據庫,還有鏈接池的功能,下面就來看看 Mybatis 本身實現的數據源頭和鏈接池的一個實現原理。
Mybatis 數據源的實現主要是在 datasource 包下:
咱們常見的數據源組件都實現了 Javax.sql.DataSource 接口,Mybatis 也實現該接口而且提供了兩個實現類 UnpooledDataSource 和 PooledDataSource 一個使用鏈接池,一個不使用鏈接池,此外,對於這兩個類,Mybatis 還提供了兩個工廠類進行建立對象,是工廠方法模式的一個應用,首先來看下它們的一個類圖:
關於上述幾個類,PooledDataSource 和 UnpooledDataSource 是數據源實現的主要邏輯,代碼比較複雜,放在後面來看,如今先看看看兩個工廠類 。
先來看看 DataSourceFactory 類,該類是 JndiDataSourceFactory 和 UnpooledDataSourceFactory 兩個工廠類的頂層接口,只定義了兩個方法,以下所示:
public interface DataSourceFactory { // 設置 DataSource 的相關屬性,通常在初始化完成後進行設置 void setProperties(Properties props); // 獲取數據源 DataSource 對象 DataSource getDataSource(); }
UnpooledDataSourceFactory 主要用來建立 UnpooledDataSource 對象,它會在構造方法中初始化 UnpooledDataSource 對象,並在 setProperties 方法中完成對 UnpooledDataSource 對象的配置
public class UnpooledDataSourceFactory implements DataSourceFactory { // 數據庫驅動前綴 private static final String DRIVER_PROPERTY_PREFIX = "driver."; private static final int DRIVER_PROPERTY_PREFIX_LENGTH = DRIVER_PROPERTY_PREFIX.length(); // 對應的數據源,即 UnpooledDataSource protected DataSource dataSource; public UnpooledDataSourceFactory() { this.dataSource = new UnpooledDataSource(); } // 對數據源 UnpooledDataSource 進行配置 @Override public void setProperties(Properties properties) { Properties driverProperties = new Properties(); // 建立 DataSource 相應的 MetaObject MetaObject metaDataSource = SystemMetaObject.forObject(dataSource); // 遍歷 properties 集合,該集合中存放了數據源須要的信息 for (Object key : properties.keySet()) { String propertyName = (String) key; // 以 "driver." 開頭的配置項是對 DataSource 的配置,記錄到 driverProperties 中 if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) { String value = properties.getProperty(propertyName); driverProperties.setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value); } else if (metaDataSource.hasSetter(propertyName)) { // 該屬性是否有 set 方法 // 獲取對應的屬性值 String value = (String) properties.get(propertyName); // 根據屬性類型進行類型的轉換,主要是 Integer, Long, Boolean 三種類型的轉換 Object convertedValue = convertValue(metaDataSource, propertyName, value); // 設置DataSource 的相關屬性值 metaDataSource.setValue(propertyName, convertedValue); } else { throw new DataSourceException("Unknown DataSource property: " + propertyName); } } // 設置 DataSource.driverProerties 屬性值 if (driverProperties.size() > 0) { metaDataSource.setValue("driverProperties", driverProperties); } } // 返回數據源 @Override public DataSource getDataSource() { return dataSource; } // 類型轉 private Object convertValue(MetaObject metaDataSource, String propertyName, String value) { Object convertedValue = value; Class<?> targetType = metaDataSource.getSetterType(propertyName); if (targetType == Integer.class || targetType == int.class) { convertedValue = Integer.valueOf(value); } else if (targetType == Long.class || targetType == long.class) { convertedValue = Long.valueOf(value); } else if (targetType == Boolean.class || targetType == boolean.class) { convertedValue = Boolean.valueOf(value); } return convertedValue; } }
JndiDataSourceFactory 依賴 JNDI 服務器中獲取用戶配置的 DataSource,這裏能夠不看。
PooledDataSourceFactory 主要用來建立 PooledDataSource 對象,它繼承了 UnpooledDataSource 類,設置 DataSource 參數的方法複用UnpooledDataSource 中的 setProperties 方法,只是數據源返回的是 PooledDataSource 對象而已。
public class PooledDataSourceFactory extends UnpooledDataSourceFactory { public PooledDataSourceFactory() { this.dataSource = new PooledDataSource(); } }
以上這些就是 Mybatis 用來建立數據源的工廠類,下面就來看下數據源的主要實現。
UnpooledDataSource 不使用鏈接池來建立數據庫鏈接,每次獲取數據庫鏈接時都會建立一個新的鏈接進行返回;
public class UnpooledDataSource implements DataSource { // 加載 Driver 類的類加載器 private ClassLoader driverClassLoader; // 數據庫鏈接驅動的相關配置 private Properties driverProperties; // 緩存全部已註冊的數據庫鏈接驅動 private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap<String, Driver>(); private String driver; private String url; private String username; private String password; // 是否自動提交 private Boolean autoCommit; // 事物隔離級別 private Integer defaultTransactionIsolationLevel; // 靜態塊,在初始化的時候,從 DriverManager 中獲取全部的已註冊的驅動信息,並緩存到該類的 registeredDrivers集合中 static { Enumeration<Driver> drivers = DriverManager.getDrivers(); while (drivers.hasMoreElements()) { Driver driver = drivers.nextElement(); registeredDrivers.put(driver.getClass().getName(), driver); } } public UnpooledDataSource() { } public UnpooledDataSource(String driver, String url, String username, String password) { this.driver = driver; this.url = url; this.username = username; this.password = password; } }
接下來看下獲取鏈接的方法:
// 獲取一個新的數據庫鏈接 @Override public Connection getConnection(String username, String password) throws SQLException { return doGetConnection(username, password); } // 根據 properties 獲取一個新的數據庫鏈接 private Connection doGetConnection(Properties properties) throws SQLException { // 初始化數據庫驅動 initializeDriver(); // 經過 DriverManager 來獲取一個數據庫鏈接 Connection connection = DriverManager.getConnection(url, properties); // 配置數據庫鏈接的 autoCommit 和隔離級別 configureConnection(connection); // 返回新鏈接 return connection; } // 初始化數據庫驅動 private synchronized void initializeDriver() throws SQLException { // 若是當前的驅動尚未註冊,則進行註冊 if (!registeredDrivers.containsKey(driver)) { Class<?> driverType; try { if (driverClassLoader != null) { driverType = Class.forName(driver, true, driverClassLoader); } else { driverType = Resources.classForName(driver); } // 建立驅動 Driver driverInstance = (Driver)driverType.newInstance(); // 向 JDBC 的 DriverManager 註冊驅動 DriverManager.registerDriver(new DriverProxy(driverInstance)); // 向本類的 registeredDrivers 註冊驅動 registeredDrivers.put(driver, driverInstance); } catch (Exception e) { throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e); } } } // 設置數據庫鏈接的 autoCommit 和隔離級別 private void configureConnection(Connection conn) throws SQLException { if (autoCommit != null && autoCommit != conn.getAutoCommit()) { conn.setAutoCommit(autoCommit); } if (defaultTransactionIsolationLevel != null) { conn.setTransactionIsolation(defaultTransactionIsolationLevel); } }
以上代碼就是 UnpooledDataSource 類的主要實現邏輯,每次獲取鏈接都是從數據庫新建立一個鏈接進行返回,又由於,數據庫鏈接的建立是一個耗時的操做,且數據庫鏈接是很是珍貴的資源,若是每次獲取鏈接都建立一個,則可能會形成系統的瓶頸,拖垮響應速度等,這時就須要數據庫鏈接池了,Mybatis 也提供了本身數據庫鏈接池的實現,就是 PooledDataSource 類。
PooledDataSource 是一個比較複雜的類,PooledDataSource 新建立數據庫鏈接是使用 UnpooledDataSource 來實現的,且 PooledDataSource 並不會管理 java.sql.Connection 對象,而是管理 PooledConnection 對象,在 PooledConnection 中封裝了真正的數據庫鏈接對象和其代理對象;此外,因爲它是一個鏈接池,因此還須要管理鏈接池的狀態,好比有多少鏈接是空閒的,還能夠建立多少鏈接,此時,就須要一個類來管理鏈接池的對象,即 PoolState 對象;先來看下 PooledDataSource 的一個 UML 圖:
先來看看 PooledConnection 類,它主要是用來管理數據庫鏈接的,它是一個代理類,實現了 InvocationHandler 接口,
class PooledConnection implements InvocationHandler { // close 方法 private static final String CLOSE = "close"; // 記錄當前的 PooledConnection 對象所在的 PooledDataSource 對象,該 PooledConnection 對象是從 PooledDataSource 對象中獲取的,當調用 close 方法時會將 PooledConnection 放回該 PooledDataSource 中去 private PooledDataSource dataSource; // 真正的數據庫鏈接 private Connection realConnection; // 數據庫鏈接的代理對象 private Connection proxyConnection; // 從鏈接池中取出該鏈接的時間戳 private long checkoutTimestamp; // 該鏈接建立的時間戳 private long createdTimestamp; // 該鏈接最後一次被使用的時間戳 private long lastUsedTimestamp; // 用於標識該鏈接所在的鏈接池,由URL+username+password 計算出來的hash值 private int connectionTypeCode; // 該鏈接是否有效 private boolean valid; // 建立鏈接 public PooledConnection(Connection connection, PooledDataSource dataSource) { this.hashCode = connection.hashCode(); this.realConnection = connection; this.dataSource = dataSource; this.createdTimestamp = System.currentTimeMillis(); this.lastUsedTimestamp = System.currentTimeMillis(); this.valid = true; this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this); } // 廢棄該鏈接 public void invalidate() { valid = false; } // 判斷該鏈接是否有效, // 1.判斷 valid 字段 // 2.向數據庫中發送檢測測試的SQL,查看真正的鏈接仍是否有效 public boolean isValid() { return valid && realConnection != null && dataSource.pingConnection(this); } // setter / getter 方法 }
接下來看下 invoke 方法,該方法是 proxyConnection 這個鏈接代理對象的真正代理邏輯,它會對 close 方法進行代理,而且在調用真正的鏈接以前對鏈接進行檢測。
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); // 若是執行的方法是 close 方法,則會把當前鏈接放回到 鏈接池中去,供下次使用,而不是真正的關閉數據庫鏈接 if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) { dataSource.pushConnection(this); return null; } else { try { // 若是不是 close 方法,則 調用 真正的數據庫鏈接執行 if (!Object.class.equals(method.getDeclaringClass())) { // 執行以前,須要進行鏈接的檢測 checkConnection(); } // 調用數據庫真正的鏈接進行執行 return method.invoke(realConnection, args); } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } }
PoolState 類主要是用來管理鏈接池的狀態,好比哪些鏈接是空閒的,哪些是活動的,還能夠建立多少鏈接等。該類中只是定義了一些屬性來進行控制鏈接池的狀態,並無任何的方法。
public class PoolState { // 該 PoolState 屬於哪一個 PooledDataSource protected PooledDataSource dataSource; // 來用存放空閒的 pooledConnection 鏈接 protected final List<PooledConnection> idleConnections = new ArrayList<PooledConnection>(); // 用來存放活躍的 PooledConnection 鏈接 protected final List<PooledConnection> activeConnections = new ArrayList<PooledConnection>(); // 請求數據庫鏈接的次數 protected long requestCount = 0; // 獲取鏈接的累計時間 protected long accumulatedRequestTime = 0; // checkoutTime 表示從鏈接池中獲取鏈接到歸還鏈接的時間 // accumulatedCheckoutTime 記錄了全部鏈接的累計 checkoutTime 時長 protected long accumulatedCheckoutTime = 0; // 鏈接超時的鏈接個數 protected long claimedOverdueConnectionCount = 0; // 累計超時時間 protected long accumulatedCheckoutTimeOfOverdueConnections = 0; // 累計等待時間 protected long accumulatedWaitTime = 0; // 等待次數 protected long hadToWaitCount = 0; // 無效的鏈接數 protected long badConnectionCount = 0; // setter / getter 方法 }
PooledDataSource 它是一個簡單的,同步的,線程安全的數據庫鏈接池
知道了 UnpooledDataSource 用來建立數據庫新的鏈接,PooledConnection 用來管理鏈接池中的鏈接,PoolState 用來管理鏈接池的狀態以後,來看下 PooledDataSource 的一個邏輯,該類中主要有如下幾個方法:獲取數據庫鏈接的方法 popConnection,把鏈接放回鏈接池的方法 pushConnection,檢測數據庫鏈接是否有效的方法 pingConnection ,還有 關閉鏈接池中全部鏈接的方法 forceCloseAll,接下來就來看看這幾個方法是怎麼實現,在看以前,先看下該方法定義的一些屬性:
public class PooledDataSource implements DataSource { // 鏈接池的狀態 private final PoolState state = new PoolState(this); // 用來建立真正的數據庫鏈接對象 private final UnpooledDataSource dataSource; // 最大活躍的鏈接數,默認爲 10 protected int poolMaximumActiveConnections = 10; // 最大空閒鏈接數,默認爲 5 protected int poolMaximumIdleConnections = 5; // 最大獲取鏈接的時長 protected int poolMaximumCheckoutTime = 20000; // 在沒法獲取到鏈接時,最大等待的時間 protected int poolTimeToWait = 20000; // 在檢測一個鏈接是否可用時,會向數據庫發送一個測試 SQL protected String poolPingQuery = "NO PING QUERY SET"; // 是否容許發送測試 SQL protected boolean poolPingEnabled; // 當鏈接超過 poolPingConnectionsNotUsedFor 毫秒未使用時,會發送一次測試 SQL 語句,測試鏈接是否正常 protected int poolPingConnectionsNotUsedFor; // 標誌着當前的鏈接池,是 url+username+password 的 hash 值 private int expectedConnectionTypeCode; // 建立鏈接池 public PooledDataSource(String driver, String url, String username, String password) { dataSource = new UnpooledDataSource(driver, url, username, password); expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword()); } // 生成 hash 值 private int assembleConnectionTypeCode(String url, String username, String password) { return ("" + url + username + password).hashCode(); } // setter / getter 方法 }
接下來看下從數據庫鏈接池中獲取鏈接的實現邏輯:
從 鏈接池中獲取鏈接的方法主要是在 popConnection 中實現的,先來看下它的一個流程圖:
代碼邏輯以下:
// 獲取鏈接 @Override public Connection getConnection(String username, String password) throws SQLException { return popConnection(username, password).getProxyConnection(); } // 從鏈接池中獲取鏈接 private PooledConnection popConnection(String username, String password) throws SQLException { // 等待的個數 boolean countedWait = false; // PooledConnection 對象 PooledConnection conn = null; long t = System.currentTimeMillis(); // 無效的鏈接個數 int localBadConnectionCount = 0; while (conn == null) { synchronized (state) { // 檢測是否還有空閒的鏈接 if (!state.idleConnections.isEmpty()) { // 鏈接池中還有空閒的鏈接,則直接獲取鏈接返回 conn = state.idleConnections.remove(0); } else { // 鏈接池中已經沒有空閒鏈接了 if (state.activeConnections.size() < poolMaximumActiveConnections) { // 活躍的鏈接數沒有達到最大值,則建立一個新的數據庫鏈接 conn = new PooledConnection(dataSource.getConnection(), this); } else { // 若是活躍的鏈接數已經達到容許的最大值了,則不能建立新的數據庫鏈接 // 獲取最早建立的那個活躍的鏈接 PooledConnection oldestActiveConnection = state.activeConnections.get(0); long longestCheckoutTime = oldestActiveConnection.getCheckoutTime(); // 檢測該鏈接是否超時 if (longestCheckoutTime > poolMaximumCheckoutTime) { // 若是該鏈接超時,則進行相應的統計 state.claimedOverdueConnectionCount++; state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime; state.accumulatedCheckoutTime += longestCheckoutTime; // 將超時鏈接移出 activeConnections 集合 state.activeConnections.remove(oldestActiveConnection); if (!oldestActiveConnection.getRealConnection().getAutoCommit()) { // 若是超時未提交,則自動回滾 oldestActiveConnection.getRealConnection().rollback(); } // 建立新的 PooledConnection 對象,可是真正的數據庫鏈接並無建立 conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this); conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp()); conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp()); // 設置該超時的鏈接爲無效 oldestActiveConnection.invalidate(); } else { // 若是無空閒鏈接,沒法建立新的鏈接且無超時鏈接,則只能阻塞等待 // Must wait try { if (!countedWait) { state.hadToWaitCount++; // 等待次數 countedWait = true; } long wt = System.currentTimeMillis(); // 阻塞等待 state.wait(poolTimeToWait); state.accumulatedWaitTime += System.currentTimeMillis() - wt; } catch (InterruptedException e) { break; } } } } // 已經獲取到鏈接 if (conn != null) { if (conn.isValid()) { // 若是連鏈接有效,事務未提交則回滾 if (!conn.getRealConnection().getAutoCommit()) { conn.getRealConnection().rollback(); } // 設置 PooledConnection 相關屬性 conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password)); conn.setCheckoutTimestamp(System.currentTimeMillis()); conn.setLastUsedTimestamp(System.currentTimeMillis()); // 把鏈接加入到活躍集合中去 state.activeConnections.add(conn); state.requestCount++; state.accumulatedRequestTime += System.currentTimeMillis() - t; } else { // 無效鏈接 state.badConnectionCount++; localBadConnectionCount++; conn = null; } } } } return conn; }
以上就是從鏈接池獲取鏈接的主要邏輯。
如今來看下當執行 close 方法的時候,會把鏈接放入的鏈接池中以供下次從新使用,把鏈接放入到鏈接池中的方法爲 pushConnection 方法,它也是 PooledDataSource 類的一個主要方法,先來看下它的流程圖:
代碼以下:
// 把不用的鏈接放入到鏈接池中 protected void pushConnection(PooledConnection conn) throws SQLException { synchronized (state) { // 首先從活躍的集合中移除掉該鏈接 state.activeConnections.remove(conn); // 檢測鏈接是否有效 if (conn.isValid()) { // 若是空閒鏈接數沒有達到最大值,且 PooledConnection 爲該鏈接池的鏈接 if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) { // 累計 checkout 時長 state.accumulatedCheckoutTime += conn.getCheckoutTime(); // 事務回滾 if (!conn.getRealConnection().getAutoCommit()) { conn.getRealConnection().rollback(); } // 爲返還的鏈接建立新的 PooledConnection 對象 PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this); // 把該鏈接添加的空閒鏈表中 state.idleConnections.add(newConn); newConn.setCreatedTimestamp(conn.getCreatedTimestamp()); newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp()); // 設置該鏈接爲無效狀態 conn.invalidate(); // 喚醒阻塞等待的線程 state.notifyAll(); } else { // 若是空閒鏈接數已經達到最大值 state.accumulatedCheckoutTime += conn.getCheckoutTime(); if (!conn.getRealConnection().getAutoCommit()) { conn.getRealConnection().rollback(); } // 則關閉真正的數據庫連擊破 conn.getRealConnection().close(); // 設置該鏈接爲無效狀態 conn.invalidate(); } } else { // 無效鏈接個數加1 state.badConnectionCount++; } } }
以上代碼就是把不用的鏈接放入到鏈接池中以供下次使用,
在上面兩個方法中,都調用了 isValid 方法來檢測鏈接是否可用,該方法除了檢測 valid 字段外,還會調用 pingConnection 方法來嘗試讓數據庫執行測試 SQL 語句,從而檢測真正的數據庫鏈接對象是否依然正常可用。
// 檢測鏈接是否可用 public boolean isValid() { return valid && realConnection != null && dataSource.pingConnection(this); } // 向數據庫發送測試 SQL 來檢測真正的數據庫鏈接是否可用 protected boolean pingConnection(PooledConnection conn) { // 結果 boolean result = true; try { // 檢測真正的數據庫鏈接是否已經關閉 result = !conn.getRealConnection().isClosed(); } catch (SQLException e) { result = false; } // 若是真正的數據庫鏈接還沒關閉 if (result) { // 是否執行測試 SQL 語句 if (poolPingEnabled) { // 長時間(poolPingConnectionsNotUsedFor 指定的時長)未使用的鏈接,才須要ping操做來檢測鏈接是否正常 if (poolPingConnectionsNotUsedFor >= 0 && conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) { try { // 發送測試 SQL 語句執行 Connection realConn = conn.getRealConnection(); Statement statement = realConn.createStatement(); ResultSet rs = statement.executeQuery(poolPingQuery); rs.close(); statement.close(); if (!realConn.getAutoCommit()) { realConn.rollback(); } result = true; } catch (Exception e) { try { conn.getRealConnection().close(); } catch (Exception e2) { } result = false; } } } } return result; }
此外,當修改 PooledDataSource 相應的字段,如 數據庫的 URL,用戶名或密碼等,須要將鏈接池中鏈接所有關閉,以後獲取鏈接的時候從從新初始化。關閉鏈接池中所有鏈接的方法爲 forceCloseAll:
public void forceCloseAll() { synchronized (state) { expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword()); // 處理活躍的鏈接 for (int i = state.activeConnections.size(); i > 0; i--) { try { PooledConnection conn = state.activeConnections.remove(i - 1); // 設置鏈接爲無效狀態 conn.invalidate(); // 獲取數據庫真正的鏈接 Connection realConn = conn.getRealConnection(); // 事物回滾 if (!realConn.getAutoCommit()) { realConn.rollback(); } // 關閉數據庫鏈接 realConn.close(); } catch (Exception e) { // ignore } } // 處理空閒的鏈接 for (int i = state.idleConnections.size(); i > 0; i--) { try { PooledConnection conn = state.idleConnections.remove(i - 1); // 設置爲無效狀態 conn.invalidate(); Connection realConn = conn.getRealConnection(); if (!realConn.getAutoCommit()) { realConn.rollback(); } realConn.close(); } catch (Exception e) { } } } }
在鏈接池中提到了 鏈接池中的最大鏈接數和最大空閒數,在 獲取鏈接和把鏈接放入鏈接池中都有判斷,
1. 獲取鏈接:首先從鏈接池中進行獲取,若是鏈接池中已經沒有空閒的鏈接了,則會判斷當前的活躍鏈接數是否已經達到容許的最大值了,若是沒有,則還能夠建立新的鏈接,以後把它放到活躍的集合中進行使用,若是當前活躍的已達到最大值,則阻塞。
2.返還鏈接到鏈接池,在返還鏈接的時候,進行判斷,若是空閒鏈接數已達到容許的最大值,則直接關閉真正的數據庫鏈接,不然把該鏈接放入到空閒集合中以供下次使用。
Mybatis 數據源中,主要的代碼邏輯仍是在鏈接池類 PooledDataSource 中,對於獲取鏈接的方法 popConnection,返還鏈接的方法 pushConnection ,須要結合上圖來看,才能看得清楚。