開發與維運

MyBatis源碼解析之基礎模塊—DataSource

The MyBatis Blog

MyBatis源碼解析之基礎模塊—DataSource

背景知識

因為常見的數據源都會基於javax.sql.Datasource實現。Mybatis的數據源實現也是基於實現javax.sql.Datasource來設計的,也是在介紹MyBatis數據源實現之前,咱們先了解下JDK的DataSource。

關於jdk中對DataSource在Oracle官網DataSource介紹有如下一段描述:

A factory for connections to the physical data source that this DataSource object represents. An alternative to the DriverManager facility, a DataSource object is the preferred means of getting a connection. An object that implements the DataSource interface will typically be registered with a naming service based on the Java™ Naming and Directory (JNDI) API.

The DataSource interface is implemented by a driver vendor. There are three types of implementations:

  1. Basic implementation -- produces a standard Connection object
  2. Connection pooling implementation -- produces a Connection object that will automatically participate in connection pooling. This implementation works with a middle-tier connection pooling manager.
  3. Distributed transaction implementation -- produces a Connection object that may be used for distributed transactions and almost always participates in connection pooling. This implementation works with a middle-tier transaction manager and almost always with a connection pooling manager.

A DataSource object has properties that can be modified when necessary. For example, if the data source is moved to a different server, the property for the server can be changed. The benefit is that because the data source's properties can be changed, any code accessing that data source does not need to be changed.

A driver that is accessed via a DataSource object does not register itself with the DriverManager. Rather, a DataSource object is retrieved though a lookup operation and then used to create a Connection object. With a basic implementation, the connection obtained through a DataSource object is identical to a connection obtained through the DriverManager facility.

An implementation of DataSource must include a public no-arg constructor.

翻譯過來就是:

一個用於連接到此DataSource對象表示的物理數據源的工廠。作為DriverManager工具的替代方法,DataSource對象是獲取連接的首選方法。通常將基於Java™命名和目錄(JNDI)API向實現命名服務的對象註冊實現DataSource接口的對象。

DataSource接口由驅動程序供應商實現。共有三種類型的實現:

基本實現-產生一個標準的Connection對象
連接池實現-產生一個Connection對象,該對象將自動參與連接池。此實現與中間層連接池管理器一起使用。
分佈式事務實現-產生一個Connection對象,該對象可用於分佈式事務,並且幾乎總是參與連接池。此實現與中間層事務管理器一起使用,並且幾乎總是與連接池管理器一起使用。
DataSource對象具有可以在必要時修改的屬性。例如,如果將數據源移動到其他服務器,則可以更改服務器的屬性。好處在於,因為可以更改數據源的屬性,所以不需要更改訪問該數據源的任何代碼。

通過DataSource對象訪問的驅動程序不會在DriverManager中註冊自身。而是通過查找操作檢索DataSource對象,然後將其用於創建Connection對象。通過基本實現,通過DataSource對象獲得的連接與通過DriverManager工具獲得的連接相同。

DataSource的實現必須包括一個公共的無參數構造函數。

根據 DataSource的實現必須包括一個公共的無參數構造函數的描述。

這也是下面分析源碼時看到的為什麼池化數據源PoolDataSource與非池化數據源UnpooledDataSource都有顯性定義無參構造函數的原因。

關於DataSource就簡單介紹到這裡,有興趣的同學可以查閱相關資料及jdk源碼等。

架構設計

DataSource模塊所在包路徑為org.apache.ibatis.datasource,其具體劃分如下:

datasource
- jndi
  - JndiDataSourceFactory
- pooled
  - PooledConnection
  - PooledDataSource
  - PooledDataSourceFactory
  - PoolState
- unpooled
  - UnpooledDataSource
  - UnpooledDataSourceFactory
- DataSourceException
- DataSourceFactory

對應的類架構設計圖如下:

mybatis-datasource-architecture.png

從架構圖中,我們很顯然發現,該架構採用經典的工廠方法設計模式(關於設計模式的介紹各位可以參閱本人的另一個設計模式專題,或者其他資料)

源碼解讀

DataSourceFactory

DataSourceFactory接口只提供兩個方法:setProperties()設置數據源相關屬性;getDataSource()獲取數據源。

/**
 * 數據源工廠接口類,只提供兩個方法:
 * 1.設置相關配置屬性
 * 2.獲取數據源
 * 3.該接口有三個實現類:PooledDataSourceFactory,UnpooledDataSourceFactory,JndiDataSourceFactory
 */
public interface DataSourceFactory {

  //設置屬性(其目的是位datasource填充配置屬性)
  void setProperties(Properties props);

  //獲取數據源對象
  DataSource getDataSource();

}

MyBatis提供三種DataSourceFactory的實現方式:JndiDataSourceFactoryUnpooledDataSourceFactoryPooledDataSourceFactory。現對其逐一介紹。

UnpooledDataSourceFactory

DataSourceFactoryUnpooledDataSourceFactory的實現,首先會在其構造方法中直接實例化非池化的數據源UnpooledDataSource ,並 通過getDataSource()方法獲取該數據源。UnpooledDataSource數據源中相關屬性的填充則通過setProperties()進行設置。具體細節及說明,請參閱如下源碼:

package org.apache.ibatis.datasource.unpooled;

import java.util.Properties;
import javax.sql.DataSource;

import org.apache.ibatis.datasource.DataSourceException;
import org.apache.ibatis.datasource.DataSourceFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;

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();

  protected DataSource dataSource;

  public UnpooledDataSourceFactory() {
    this.dataSource = new UnpooledDataSource();
  }

  /**
   * 從properties中獲取對應的配置信息
   */
  @Override
  public void setProperties(Properties properties) {
    Properties driverProperties = new Properties();
    MetaObject metaDataSource = SystemMetaObject.forObject(dataSource);
    for (Object key : properties.keySet()) {
      String propertyName = (String) key;
      if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) {
        //"driver."開頭的為DataSource相關配置,均保存到driverProperties對象中,並最終會設置在metaDataSource中
        String value = properties.getProperty(propertyName);
        driverProperties.setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value);
      } else if (metaDataSource.hasSetter(propertyName)) {
        //普通配置屬性直接設置在metaDataSource中
        String value = (String) properties.get(propertyName);
        //調用私有類型轉換方法
        Object convertedValue = convertValue(metaDataSource, propertyName, value);
        metaDataSource.setValue(propertyName, convertedValue);
      } else {
        throw new DataSourceException("Unknown DataSource property: " + propertyName);
      }
    }
    /** driverProperties對象有值的情況下才設置 */
    if (driverProperties.size() > 0) {
      metaDataSource.setValue("driverProperties", driverProperties);
    }
  }

  @Override
  public DataSource getDataSource() {
    return dataSource;
  }

  /** 根據propertyName的屬性類型轉換對應的value值,主要處理Integer,long及boolean三種 */
  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;
  }

}

PooledDataSourceFactory

PooledDataSourceFactory實現則更為簡單,他並沒有複寫setProperties()getDataSource()方法,而是直接繼承UnpooledDataSourceFactory,唯一區別就是構造方法中dataSource實例化對象為PooledDataSource,具體代碼如下

public class PooledDataSourceFactory extends UnpooledDataSourceFactory {

  public PooledDataSourceFactory() {
    //與UnpooledDataSourceFactory唯一的區別
    this.dataSource = new PooledDataSource();
  }
}

JndiDataSourceFactory

介紹JndiDataSourceFactory之前,先簡單介紹下JNDI(全稱為Java Naming and Directory Interface), JNDI是 SUN 公司提供的一種標準的 Java 命名系統接口,JNDI 提供統一的客戶端 API,通過不同的訪問提供者接口 JNDI 服務供應接口 ( SPI ) 的實現,由管理者將 JNDI API 映射為特定的命名服務和目錄系統,使得 Java 應用程序可以和這些命名服務和目錄服務之間進行交互。其根本目的還是降低耦合性,提供部署的靈活性,降低維護成本。

JndiDataSourceFactory實現與上述兩種有所不同,從其命名的方式就可以知道DataSource的設置需要依賴具體的數據源廠商,因此不能在構造函數中進行實例化。所以只能從配置中讀取相關信息,然後根據Context上下文環境,通過lookup的方式進行實例化。

具體細節及說明,請看如下代碼及相關注釋:

package org.apache.ibatis.datasource.jndi;

import java.util.Map.Entry;
import java.util.Properties;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

import org.apache.ibatis.datasource.DataSourceException;
import org.apache.ibatis.datasource.DataSourceFactory;

/**
 * Jndi數據源工廠類
 */
public class JndiDataSourceFactory implements DataSourceFactory {

  public static final String INITIAL_CONTEXT = "initial_context";
  public static final String DATA_SOURCE = "data_source";
  public static final String ENV_PREFIX = "env.";

  private DataSource dataSource;

  /**
   * 根據傳入的properties傳入的屬性,獲取以"env."開頭的鍵值對屬性,並存入env 對象中
   * 如果env為null,則調用InitialContext無參構造函數
   * 如果env不為null,則根據調用InitialContext 有參構造函數將env對象傳入,進行初始化。
   *
   * 若傳入的properties中存在initial_context屬性,且存在data_source屬性,則先根據initial_context作為key通過initCtx進行查找。
   *    然後根據查找出的ctx對象繼續以data_source為key進行查找,並將返回的對象轉換為DataSource數據源
   * 否則 如果properties中包含data_source 的key,則直接調用initCtx獲取對象並返回DataSource數據源
   * */
  @Override
  public void setProperties(Properties properties) {
    try {
      InitialContext initCtx;
      Properties env = getEnvProperties(properties);
      if (env == null) {
        initCtx = new InitialContext();
      } else {
        initCtx = new InitialContext(env);
      }

      if (properties.containsKey(INITIAL_CONTEXT)
          && properties.containsKey(DATA_SOURCE)) {
        Context ctx = (Context) initCtx.lookup(properties.getProperty(INITIAL_CONTEXT));
        dataSource = (DataSource) ctx.lookup(properties.getProperty(DATA_SOURCE));
      } else if (properties.containsKey(DATA_SOURCE)) {
        dataSource = (DataSource) initCtx.lookup(properties.getProperty(DATA_SOURCE));
      }

    } catch (NamingException e) {
      throw new DataSourceException("There was an error configuring JndiDataSourceTransactionPool. Cause: " + e, e);
    }
  }

  @Override
  public DataSource getDataSource() {
    return dataSource;
  }

  /** 根據properties中屬性信息獲取以"env."開頭的配置信息,並將其(去除"env."的key)鍵值對添加到contextProperties中 */
  private static Properties getEnvProperties(Properties allProps) {
    final String PREFIX = ENV_PREFIX;
    Properties contextProperties = null;
    for (Entry<Object, Object> entry : allProps.entrySet()) {
      String key = (String) entry.getKey();
      String value = (String) entry.getValue();
      if (key.startsWith(PREFIX)) {
        if (contextProperties == null) {
          contextProperties = new Properties();
        }
        contextProperties.put(key.substring(PREFIX.length()), value);
      }
    }
    return contextProperties;
  }
}

介紹完DataSourceFactory的幾種實現,下面咱們一起來看下DataSource的兩種實現:UnpooledDataSourcePooledDataSource。由於PooledDataSource實現中會依賴UnpooledDataSource,所以咱們先看下UnpooledDataSource

UnpooledDataSource

首先看下UnpooledDataSource類的幾個屬性:

  private ClassLoader driverClassLoader; //driver的類加載器
  private Properties driverProperties; //數據庫相關的驅動配置信息
  private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap<>(); //從DriverManager中已註冊的驅動複製而來

  private String driver; //驅動類全路徑名稱
  private String url; //數據庫連接地址
  private String username; //數據庫用戶名
  private String password; //數據庫密碼

  private Boolean autoCommit; //是否自動提交
  private Integer defaultTransactionIsolationLevel; //事務隔離級別
  private Integer defaultNetworkTimeout; //默認網絡超時時間

其中 registeredDrivers,該屬性的設置時通過靜態代碼塊進行初始化,從DriverManager中已註冊的驅動中複製一份。代碼如下:

/** 初始化註冊的驅動 */
static {
  Enumeration<Driver> drivers = DriverManager.getDrivers();
  while (drivers.hasMoreElements()) {
    Driver driver = drivers.nextElement();
    registeredDrivers.put(driver.getClass().getName(), driver);
  }
}

然後是UnpooledDataSource的多個構造方法的重載,以及對javax.sql.Datasource接口的方法實現,還有就是對上述屬性對應的getter/setter方法。內容比較簡單不再描述。

這裡重點要說下方法UnpooledDataSource#doGetConnection(java.lang.String, java.lang.String),因為UnpooledDataSource#getConnection()所有重載方法最終都會調用該方法。doGetConnection(java.lang.String, java.lang.String)方法主要邏輯是組裝用戶名、密碼及registeredDrivers到properties中,然後調用重載方法 org.apache.ibatis.datasource.unpooled.UnpooledDataSource#doGetConnection(java.util.Properties),代碼如下:

private Connection doGetConnection(String username, String password) throws SQLException {
  Properties props = new Properties();
  if (driverProperties != null) {
    props.putAll(driverProperties);
  }
  if (username != null) {
    props.setProperty("user", username);
  }
  if (password != null) {
    props.setProperty("password", password);
  }
  return doGetConnection(props);
}

通過doGetConnection重載方法最終獲取數據庫連接,如下:

private Connection doGetConnection(Properties properties) throws SQLException {
    //初始化驅動
    initializeDriver();
    //根據數據庫地址及相關屬性獲取connection
    Connection connection = DriverManager.getConnection(url, properties);
    //設置connection其他屬性值,比如網絡超時時間、是否自動提交、事務隔離級別等
    configureConnection(connection);
    //返回connection對象
    return connection;
  }

  /**
   * 初始化驅動:
   * 1、首先判斷registeredDrivers是否已存在UnpooledDataSource定義的driver屬性
   * 2、如果不存在
   *    2.1、如果driverClassLoader 不為空,則通過驅動類加載器獲取驅動類型;否則通過Resources.classForName獲取
   *    2.2、根據驅動類類型創建對應的driver實例,並將該driver實例的代理對象添加到DriverManager以及registeredDrivers中
   * 否則拋出驅動類初始化異常
   */
  private synchronized void initializeDriver() throws SQLException {
    System.out.println("**************** driver:"+driver);
    if (!registeredDrivers.containsKey(driver)) {
      Class<?> driverType;
      try {
        if (driverClassLoader != null) {
          driverType = Class.forName(driver, true, driverClassLoader);
        } else {
          driverType = Resources.classForName(driver);
        }
        // DriverManager requires the driver to be loaded via the system ClassLoader.
        // http://www.kfu.com/~nsayer/Java/dyn-jdbc.html
        Driver driverInstance = (Driver)driverType.getDeclaredConstructor().newInstance();
        DriverManager.registerDriver(new DriverProxy(driverInstance));
        registeredDrivers.put(driver, driverInstance);
      } catch (Exception e) {
        throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
      }
    }
  }

  /** 設置connection其他屬性值,比如網絡超時時間、是否自動提交、事務隔離級別等 */
  private void configureConnection(Connection conn) throws SQLException {
    if (defaultNetworkTimeout != null) {
      conn.setNetworkTimeout(Executors.newSingleThreadExecutor(), defaultNetworkTimeout);
    }
    if (autoCommit != null && autoCommit != conn.getAutoCommit()) {
      conn.setAutoCommit(autoCommit);
    }
    if (defaultTransactionIsolationLevel != null) {
      conn.setTransactionIsolation(defaultTransactionIsolationLevel);
    }
  }

至此,關於 UnpooledDataSource核心內容的介紹已完成。

但是,我們在實際的開發實踐中,並不會採用非池化的數據源。我相信熟悉JDBC編程的小夥伴應該知道,數據庫連接的創建過程是比較耗時的,有可能數據庫的連接創建耗時比真正的數據庫執行還要長。而且數據庫的鏈接數資源就像內存一樣,既寶貴又有限。為了減少這種資源浪費,聰明的程序員提出連接池這種方案。這樣就避免了大量的連接池創建耗時,是的連接可以重用。理想很豐滿,現實很骨感。雖然這種方案解決了連接的創建問題,但是到底多大的連接數合適呢, 經過大量的應用實踐,mybatis給出了比較好的方案,根據應用的業務量通過動態配置的方式來解決該問題。下面我們繼續學習下PooledDataSource相關內容。

介紹PooledDataSource之前,我們回顧下上面的架構圖。從架構圖中我們可以看出,PooledDataSource並不會直接維護javax.sql.Connection,而是通過PooledConnection來間接管理。

PooledConnection

PooledConnection 類實現了 InvocationHandler 接口,顯然也是採用了JDK動態代理的模式。該類有三塊核心點:擁有的相關屬性、構造方法、及實現的invoker方法。具體說明及業務邏輯如下:

class PooledConnection implements InvocationHandler {

  private static final String CLOSE = "close"; //調用的是否為close關閉連接對象方法
  private static final Class<?>[] IFACES = new Class<?>[] { Connection.class };

  private final int hashCode; //連接的hash碼
  private final PooledDataSource dataSource; //連接對象所屬的池化數據源
  private final Connection realConnection; //真正的連接對象
  private final Connection proxyConnection; //代理連接對象
  private long checkoutTimestamp; //連接對象檢出的時間戳
  private long createdTimestamp; //連接對象創建時間戳
  private long lastUsedTimestamp; //連接對象最後一次使用的時間戳
  private int connectionTypeCode; //基於數據庫URL、用戶名、密碼生成的連接類型code
  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);
  }
  
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String methodName = method.getName();
    // 判斷方法是否為close,是則將調用pushConnection方法將該連接返回到空閒列表
    if (CLOSE.equals(methodName)) {
      dataSource.pushConnection(this);
      return null;
    }
    try {
      if (!Object.class.equals(method.getDeclaringClass())) {
        // issue #579 toString() should never fail
        // throw an SQLException instead of a Runtime
        // 檢查連接對象是否有效
        checkConnection();
      }
      // 調用真正的連接並返回
      return method.invoke(realConnection, args);
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }
}

PoolState

PooledDataSource類中,還有個組件是PoolState,該對象主要管理連接對象相關的狀態信息,比如空閒列表、活躍連接、超時時間等,詳細見代碼:

public class PoolState {

  protected PooledDataSource dataSource; //連接池數據源(通過構造方法設置)

  protected final List<PooledConnection> idleConnections = new ArrayList<>(); //空閒連接對象列表
  protected final List<PooledConnection> activeConnections = new ArrayList<>(); //激活使用中的連接對象列表
  protected long requestCount = 0; //請求數
  protected long accumulatedRequestTime = 0; //累計連接時間
  protected long accumulatedCheckoutTime = 0; //累計檢出時間
  protected long claimedOverdueConnectionCount = 0; //超時的連接個數
  protected long accumulatedCheckoutTimeOfOverdueConnections = 0; //累計超時時間
  protected long accumulatedWaitTime = 0; //累計等待時間
  protected long hadToWaitCount = 0; //等待次數
  protected long badConnectionCount = 0; //無效的連接數

  public PoolState(PooledDataSource dataSource) {
    this.dataSource = dataSource;
  }
}

PoolState中個屬性值會在不斷調用、回收、及檢測中更新值。

PooledDataSource

終於可以講解PooledDataSource了, 這是DataSource章節中的重中之重。PooledDataSource代碼較多,我們只需要關注幾個核心內容:

PooledDataSource 相關屬性對象:

  private final PoolState state = new PoolState(this); //數據源狀態維護

  private final UnpooledDataSource dataSource; //通過UnpooledDataSource獲取真正的連接,該對象在構造方法中進行實例化

  // OPTIONAL CONFIGURATION FIELDS
  protected int poolMaximumActiveConnections = 10;//最大活躍數
  protected int poolMaximumIdleConnections = 5; //最大空閒連接數
  protected int poolMaximumCheckoutTime = 20000; //最大檢出時間
  protected int poolTimeToWait = 20000; //連接阻塞需要等待的時間
  protected int poolMaximumLocalBadConnectionTolerance = 3; //可容忍的連接池最大無效連接數
  protected String poolPingQuery = "NO PING QUERY SET"; //ping 語句
  protected boolean poolPingEnabled; //是否允許發送檢查sql語句
  protected int poolPingConnectionsNotUsedFor; //連接超時的閾值,超過該閾值會發送一次連接測試sql,檢查連接是否可用

  private int expectedConnectionTypeCode; //期望數據源連接碼:url+username+password 進行hash計算獲取

forceCloseAll(),該方法關閉所有的空閒連接對象、激活連接對象。該方法會在設置數據源屬性時調用,比如設置driver、url、username、password、autoCommint等

/**
* 分別遞歸倒序循環激活連接列表,空閒連接列表。
* 將連接對象從對應列表移除,並將連接對象設置為無效。同時判斷該真正連接是否設置自動提交,若不是則進行rollback回滾。
*/
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) {
        // ignore
      }
    }
  }
  if (log.isDebugEnabled()) {
    log.debug("PooledDataSource forcefully closed/removed all connections.");
  }
}

PooledDataSource#getConnection()獲取連接對象並不是每次都會新創建新的連接,而是看空閒列表中是否存在空閒連接,有就直接獲取一個。具體是通過調用popConnection方法獲取連接對象。因為此處邏輯比較複雜,先看下邏輯圖,然後再分析源碼。

mybatis-datasource-popConnection.png

上圖為popConnection獲取連接對象的主要流程圖,當然,一些state相關的參數設置忽略了,詳情看如下代碼:

/** 從連接池獲取連接對象 */
private PooledConnection popConnection(String username, String password) throws SQLException {
  boolean countedWait = false;
  PooledConnection conn = null;
  long t = System.currentTimeMillis();
  int localBadConnectionCount = 0;

  while (conn == null) {
    synchronized (state) {
      if (!state.idleConnections.isEmpty()) {
        // Pool has available connection
        conn = state.idleConnections.remove(0);
        if (log.isDebugEnabled()) {
          log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
        }
      } else {
        // Pool does not have available connection
        if (state.activeConnections.size() < poolMaximumActiveConnections) {
          // Can create new connection
          conn = new PooledConnection(dataSource.getConnection(), this);
          if (log.isDebugEnabled()) {
            log.debug("Created connection " + conn.getRealHashCode() + ".");
          }
        } else {
          // Cannot create new connection
          // 獲取最早的一個活躍連接數
          PooledConnection oldestActiveConnection = state.activeConnections.get(0);
          long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
          if (longestCheckoutTime > poolMaximumCheckoutTime) {
            // Can claim overdue connection
            state.claimedOverdueConnectionCount++;
            state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
            state.accumulatedCheckoutTime += longestCheckoutTime;
            state.activeConnections.remove(oldestActiveConnection);
            if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
              try {
                oldestActiveConnection.getRealConnection().rollback();
              } catch (SQLException e) {
                /*
                   Just log a message for debug and continue to execute the following
                   statement like nothing happened.
                   Wrap the bad connection with a new PooledConnection, this will help
                   to not interrupt current executing thread and give current thread a
                   chance to join the next competition for another valid/good database
                   connection. At the end of this loop, bad {@link @conn} will be set as null.
                 */
                log.debug("Bad connection. Could not roll back");
              }
            }
            //重置最早連接對象的相關信息,並設置相關屬性
            conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
            conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
            conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
            oldestActiveConnection.invalidate();
            if (log.isDebugEnabled()) {
              log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
            }
          } else {
            // Must wait
            try {
              if (!countedWait) {
                state.hadToWaitCount++;
                countedWait = true;
              }
              if (log.isDebugEnabled()) {
                log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
              }
              long wt = System.currentTimeMillis();
              //設置組著等待,等待時長為poolTimeToWait
              state.wait(poolTimeToWait);
              // 設置總的等待時間
              state.accumulatedWaitTime += System.currentTimeMillis() - wt;
            } catch (InterruptedException e) {
              break;
            }
          }
        }
      }
      if (conn != null) {
        // ping to server and check the connection is valid or not
        // 通過ping驗證連接的有效性,若有效則將該連接對象添加到活躍列表中,並將連接請求數+1,加總連接請求時間等。
        if (conn.isValid()) {
          if (!conn.getRealConnection().getAutoCommit()) {
            conn.getRealConnection().rollback();
          }
          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 {
          if (log.isDebugEnabled()) {
            log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
          }
          //無效連接數+1
          state.badConnectionCount++;
          localBadConnectionCount++;
          conn = null;
          if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
            if (log.isDebugEnabled()) {
              log.debug("PooledDataSource: Could not get a good connection to the database.");
            }
            throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
          }
        }
      }
    }

  }

介紹完獲取連接對象,下面咱們看下PooledDataSource如何回收連接的。咱們也同樣先看下業務邏輯圖:

mybatis-datasource-pushConnection.png

具體邏輯及源碼分析見代碼:

protected void pushConnection(PooledConnection conn) throws SQLException {
  synchronized (state) {
    // 從活躍列表刪除
    state.activeConnections.remove(conn);
    // 判斷連接有效性
    if (conn.isValid()) {
      // 判斷空閒列表是否小於最大值並且是否為同一個數據源
      if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
        // 加總連接獲取時間
        state.accumulatedCheckoutTime += conn.getCheckoutTime();
        // 若非自動提交,則回滾
        if (!conn.getRealConnection().getAutoCommit()) {
          conn.getRealConnection().rollback();
        }
        // 充值真實連接的相關屬性,並添加到空閒列表中
        PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
        state.idleConnections.add(newConn);
        newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
        newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
        // 當前連接置為無效
        conn.invalidate();
        if (log.isDebugEnabled()) {
          log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
        }
        // 喚醒所有加鎖等待
        state.notifyAll();
      } else {
        // 加總連接獲取時間
        state.accumulatedCheckoutTime += conn.getCheckoutTime();
        // 若非自動提交,則回滾
        if (!conn.getRealConnection().getAutoCommit()) {
          conn.getRealConnection().rollback();
        }
        // 關閉真正的連接
        conn.getRealConnection().close();
        if (log.isDebugEnabled()) {
          log.debug("Closed connection " + conn.getRealHashCode() + ".");
        }
        // 將連接置為無效
        conn.invalidate();
      }
    } else {
      if (log.isDebugEnabled()) {
        log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
      }
      state.badConnectionCount++;
    }
  }
}

在popConnection、pushConnection中都會調用conn.isValid()判斷連接是否有效,我們先看代碼邏輯:

public boolean isValid() {
  //只有在valid為true,真實連接不為null且dataSource能夠ping檢測通過的條件下才會true
  return valid && realConnection != null && dataSource.pingConnection(this);
}
//嘗試ping進行驗證conn的可用性
protected boolean pingConnection(PooledConnection conn) {
    boolean result = true;

    try {
      result = !conn.getRealConnection().isClosed();
    } catch (SQLException e) {
      if (log.isDebugEnabled()) {
        log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
      }
      result = false;
    }

    if (result) {
      if (poolPingEnabled) {
        if (poolPingConnectionsNotUsedFor >= 0 && conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) {
          try {
            if (log.isDebugEnabled()) {
              log.debug("Testing connection " + conn.getRealHashCode() + " ...");
            }
            Connection realConn = conn.getRealConnection();
            try (Statement statement = realConn.createStatement()) {
              statement.executeQuery(poolPingQuery).close();
            }
            if (!realConn.getAutoCommit()) {
              realConn.rollback();
            }
            result = true;
            if (log.isDebugEnabled()) {
              log.debug("Connection " + conn.getRealHashCode() + " is GOOD!");
            }
          } catch (Exception e) {
            log.warn("Execution of ping query '" + poolPingQuery + "' failed: " + e.getMessage());
            try {
              conn.getRealConnection().close();
            } catch (Exception e2) {
              //ignore
            }
            result = false;
            if (log.isDebugEnabled()) {
              log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
            }
          }
        }
      }
    }
    return result;
  }

至此,DataSource模塊相關的核心要點均已介紹完畢。

總結

DataSource模塊也是採用了工廠方法、JDK動態代理等設計模式。

關於MyBatis的DataSource模塊介紹至此告一段落。感謝垂閱,如有不妥之處請多多指教~


微觀世界,達觀人生。

做一名踏實的coder !

歡迎關注我的個人微信公眾號 : todobugs

Leave a Reply

Your email address will not be published. Required fields are marked *