首頁 > 軟體

MyBatis基礎支援DataSource實現原始碼解析

2023-02-06 06:02:37

DataSource

在資料庫應用中,使用者端與資料庫伺服器端建立的連線物件(Connection)是寶貴的資源,每次請求資料庫都建立連線,使用完畢後會銷燬連線,這是一種很浪費資源的操作。因此Java提出了DataSource介面。可以把它當作一個連線池。程式初始化時,建立一批連線放入到連線池中,如果需要請求資料庫就從連線池中取出連線物件(Connection)使用完畢後把連線歸還給連線池。這樣就減少了每次請求都建立、銷燬連線的步驟,從而提高資料庫效能。

package javax.sql;
public interface DataSource  extends CommonDataSource, Wrapper {
  // 最重要的方法
  Connection getConnection() throws SQLException;
  // 其他方法不再列出
}

Java只是在JDK1.4版本釋出了該介面規範。具體實現需要使用者自己實現。MyBatis中提供了3種DataSource介面的實現。

  • UnpooledDataSource
  • PooledDataSource
  • JNDI方式的介面(不在本文討論範圍)

下面著重分析1和2這兩種DataSource的實現。

UnpooledDataSource

UnpooledDataSource顧名思義,他是非池化的DataSource,說白了和普通的Connection沒什麼區別。通過UnpooledDataSource過去連線每次都需要重新建立一個Connection。我們來看下它的getConnection實現方法。

public Connection getConnection() throws SQLException {
  return doGetConnection(username, password);
}
private Connection doGetConnection(Properties properties) throws SQLException {
  initializeDriver();
  Connection connection = DriverManager.getConnection(url, properties);
  configureConnection(connection);
  return connection;
}

在UnpooledDataSource#getConnection方法中,呼叫了doGetConnection方法,引數是username和password,該方法也就是通過使用者名稱和密碼獲取資料庫連線的意思。doGetConnection具體實現就使用了DriverManager來獲取連線物件。這是JDBC原生獲取連線物件的方式。

值得一說的是:UnpooledDataSource的其他方法都是基於DriverManager實現的。也就是說,使用UnpooledDataSource作為連線池的話等價於沒有使用連線池。

PooledDataSource

PooledDataSource才是真正意義上的連線池,它提供了連線池的大小(預設10)、最大活躍連線數量、空閒連線數量等蠶食設定。並且對Connection物件進行了JDK動態代理,重寫了Connection的close方法。使得Connection物件在呼叫close方法是不是真正的關閉連線,而是把自定義關閉行為,MyBatis的關閉邏輯就是把Connection物件歸還連線池。

我們先看下PooledDataSource的幾個重要欄位資訊

public class PooledDataSource implements DataSource {
  // PooledDataSource真正管理連線狀態的是PoolState,後面會詳細說明
  private final PoolState state = new PoolState(this);
  // UnpooledDataSource上面說過和普通的Connection無異
  private final UnpooledDataSource dataSource;
  //正在使用連線的數量
  protected int poolMaximumActiveConnections = 10;
  //空閒連線數
  protected int poolMaximumIdleConnections = 5;
  //在被強制返回之前,池中連線被檢查的時間
  protected int poolMaximumCheckoutTime = 20000;
  //這是給連線池一個列印紀錄檔狀態機會的低層次設定,還有重新 嘗試獲得連線, 這些情況下往往需要很長時間 為了避免連線池沒有設定時靜默失 敗)。
  protected int poolTimeToWait = 20000;
  //傳送到資料的偵測查詢,用來驗證連線是否正常工作,並且準備 接受請求。預設是「NO PING QUERY SET」 ,這會引起許多資料庫驅動連線由一 個錯誤資訊而導致失敗
  protected String poolPingQuery = "NO PING QUERY SET";
  //開啟或禁用偵測查詢
  protected boolean poolPingEnabled = false;
  //用來設定 poolPingQuery 多次時間被用一次
  protected int poolPingConnectionsNotUsedFor = 0;
  private int expectedConnectionTypeCode;
}

這些欄位主要記錄了連線池的重要資訊:連線池大小、空閒時最大連線數、最大活躍連線數、超時時間等。而整整揭開PooledDataSource獲取連線物件的神祕面紗還需要介紹兩個類。PooledConnection和PoolState

PooledConnection

PooledConnection實現了InvocationHandler介面,他是用來做JDK動態代理的。前文提到過,mybatis使用JDK動態代理重寫了Connection物件的close方法,就是在該類中實現的邏輯。該類有幾個重要屬性。

  • private PooledDataSource dataSource; // dataSource的副本
  • private Connection realConnection; // 真實連線物件
  • private Connection proxyConnection; // 實際返回的代理物件

接下來來看下代理物件的invoke方法是如何重寫close方法的。

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  String methodName = method.getName();
  //如果呼叫close的話,忽略它,反而將這個connection加入到池中
  if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
    dataSource.pushConnection(this);
    return null;
  } 
  return method.invoke(realConnection, args);
  // 其他邏輯省略....
}

在invoke方法中判斷下執行的方法名稱是否是Close,如果是,就不再執行原來的close方法了,而是執行PooledDataSource 的pushConnection方法!從方法名可以看出方法的作用是:把連線push到連線池PooledDataSource 中。pushConnection的邏輯後文詳細說明

PoolState

上文提到PooledDataSource並不管理連線物件。那麼程式初始化的時候建立的一批連線存放到哪裡了呢?答案是存在PoolState物件中,而PooledDataSource有一個屬性就是PoolState。也就是說PooledDataSource是通過PoolState來管理連線池的。

一批連線在Java中就是一個List集合嘛。那麼我們想一下PoolState都需要怎麼管理連線呢?首先根據連線的狀態,可以把連線分為2種

  • 空閒連線protected final List<PooledConnection> idleConnections = new ArrayList<PooledConnection>();
  • 活躍連線protected final List<PooledConnection> activeConnections = new ArrayList<PooledConnection>();

PoolState中兩個List屬性分別儲存空閒連線和活躍連線。需要連線的時候就從idleConnections 列表中取,關聯連線時就把連線從activeConnections 中移到idleConnections 中。

PoolState中還有一些其他的統計資訊欄位,比如 請求次數、請求的總時間、總連線數等這些屬性比較簡單就不再列出了

獲取連線

介紹完PooledConnection和PoolState這兩個類後,我們來看下PooledDataSource是怎麼獲取連線的。獲取連線的邏輯在PooledDataSource#getConnection方法中,getConnection方法只是一個殼子,具體呼叫邏輯在popConnection方法。我們來看一下(我只列出了重要邏輯)

public Connection getConnection() throws SQLException {
  return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
}
private PooledConnection popConnection(String username, String password) throws SQLException {
  //最外面是while死迴圈,如果一直拿不到connection,則不斷嘗試
  while (conn == null) {
    synchronized (state) {
      if (!state.idleConnections.isEmpty()) {
        //如果有空閒的連線的話,返回第一個空閒連線
        conn = state.idleConnections.remove(0);
      } else {
        //如果沒有空閒的連線
        if (state.activeConnections.size() &lt; poolMaximumActiveConnections) {
          //如果activeConnections太少,那就new一個PooledConnection
          conn = new PooledConnection(dataSource.getConnection(), this);
        } else {
          //如果activeConnections已經很多了,那不能再new了
          //取得activeConnections列表的第一個(最老的)
          PooledConnection oldestActiveConnection = state.activeConnections.get(0);
          long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
          if (longestCheckoutTime &gt; poolMaximumCheckoutTime) {
            //如果checkout時間過長,則這個connection標記為overdue(過期)
            //刪掉最老的連線,然後再new一個新連線
            conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
            oldestActiveConnection.invalidate();
          } else {
            //如果checkout時間不夠長,沒辦法,只能等待,在此分支會記錄一些統計資訊
          }
        }
      }
      if (conn != null) {
        if (conn.isValid()) {
          //如果已經拿到connection,則記錄一些統計資訊
        } else {
          //如果沒拿到,統計資訊:壞連線+1
          state.badConnectionCount++;
          localBadConnectionCount++;
          conn = null;
          //如果好幾次都拿不到,就放棄了,丟擲異常
        }
      }
    }
  }
  return conn;
}

在popConnection中

  • 從PoolState物件的空閒連線列表中獲取連線,如果有空閒連線就返回。
  • 從PoolState物件的活躍連線列表中獲取連線,如果連線數小於最大活躍數,則new一個連線返回。如果沒有隻能等待其他執行緒釋放連線再進行獲取
  • 無論是否獲取到連線,對連線進行一些資訊統計並記錄到PoolState物件中。一旦嘗試獲取連線的時間超過了閾值,就會放棄獲取連線丟擲異常

關閉連線

在PooledConnection小節中見到,PooledConnection重寫了Connection的close方法。當呼叫Connection的close方法時真正執行的邏輯是PooledDataSource的pushConnection方法。該程式碼邏輯很簡單,大體上說,就是把連線從活躍列表中刪除,加入到空閒列表中。具體實現如下

protected void pushConnection(PooledConnection conn) throws SQLException {
  synchronized (state) {
    //先從activeConnections中刪除此connection
    state.activeConnections.remove(conn);
    if (conn.isValid()) {
      if (state.idleConnections.size() &lt; poolMaximumIdleConnections &amp;&amp; conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
        //如果空閒的連線太少,
        state.accumulatedCheckoutTime += conn.getCheckoutTime();
        if (!conn.getRealConnection().getAutoCommit()) {
          conn.getRealConnection().rollback();
        }
        //new一個新的Connection,加入到idle列表
        PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
        state.idleConnections.add(newConn);
        //通知其他執行緒可以來搶connection了
        state.notifyAll();
      } else {
       //否則,即空閒的連線已經足夠了
        state.accumulatedCheckoutTime += conn.getCheckoutTime();
        //那就將connection關閉就可以了,獲取真正的connection物件並且關閉
        conn.getRealConnection().close();
        conn.invalidate();
      }
    } 
  }
}

關閉過程:

  • 空閒連線數<最大空閒連線數 則新建一個連線存放到PoolState的空閒列表中並通知其他執行緒可以來搶Connection物件
  • 如果PoolState的空閒列表是滿的,那隻能獲取真正的connection物件並將其關閉了。

小結

  • PooledDataSource真正意義上實現了DataSource介面。具有連線池的意義
  • PooledDataSource通過PooledConnection和PoolState來管理連線池中的連線
  • PooledConnection重寫了Connection物件的close方法。呼叫Connection的close方法時並不會真正的關閉連線,而是先要進行歸還連線的操作。
  • PoolState是對連線列表狀態的管理。它有兩個List屬性,分別儲存了活躍連線列表空閒連線列表

DataSourceFactory

獲取MyBatis提供的DataSource實現,需要通過工廠DataSourceFactory介面來獲取。在這裡MyBatis使用了工廠方法模式。DataSourceFactory有兩個實現類。分別是

  • UnpooledDataSourceFactory
  • PooledDataSourceFactory

我們首先來看下工廠介面定義

public interface DataSourceFactory {
  //設定屬性,被XMLConfigBuilder所呼叫
  void setProperties(Properties props);
  //生產資料來源,直接得到javax.sql.DataSource
  DataSource getDataSource();
}

其中最重要的方法就是getDataSource,它很直觀,通過工廠物件的該方法可以獲取DataSource實現。

UnpooledDataSourceFactory

UnpooledDataSourceFactory獲取dataSource的方法非常簡單直觀。

首先,構造方法裡裡new了一個UnpooledDataSource物件存放到工廠的屬性中

然後,getDataSource直接返回該物件即可。具體實現如下

public class UnpooledDataSourceFactory implements DataSourceFactory {
  protected DataSource dataSource;
  public UnpooledDataSourceFactory() {
    this.dataSource = new UnpooledDataSource();
  }
  public DataSource getDataSource() {
    return dataSource;
  }
}

PooledDataSourceFactory

PooledDataSourceFactory就有意思了,想偷懶,直接繼承自UnpooledDataSourceFactory。只需要在構造方法中new一個PooledDataSource物件,再通過getDataSource方法獲取即可。

public class PooledDataSourceFactory extends UnpooledDataSourceFactory {
  //資料來源換成了PooledDataSource
  public PooledDataSourceFactory() {
    this.dataSource = new PooledDataSource();
  }
}

結語

個人感覺mybatis提供的DataSourceFactory的實現類有點雞肋。可以說還是new物件。我們知道工廠模式建立的一般都是比較複雜的物件,是用來幫助開發者遮蔽複雜的細節。而mybatis的這兩個實現都只是new物件而已。

以上就是MyBatis基礎支援DataSource實現原始碼解析的詳細內容,更多關於MyBatis基礎支援DataSource的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com