手寫MyBatis數據庫連接池

1、資源池(Pool)技術

資源池(Resource Pool)是一種設計模式,預先構建好N個資源,需要的時候直接從池子裏面拿,用完再放回去。
預先構建好資源,節省了構建資源的時間,可以提升應用程序的響應速度。
資源使用完畢後放回池子裏,讓其他線程可以複用資源,避免了資源反覆創建和銷燬的開銷。

基於這種設計模式,於是就有了:線程池,連接池,內存池,對象池等池技術。

線程的創建和銷燬開銷是很大的,如果每執行一個異步任務都開啓一個線程,那麼很可能線程開啓和銷燬的開銷比任務執行本身的開銷都大,這樣就顯得得不償失。

和線程池類似,如果每次執行SQL都去開啓一個MySQL連接,執行完SQL再關閉連接,那麼很可能連接的時間比SQL執行本身的開銷都大。
因此,在實際項目中,幾乎都會使用數據庫連接池,而不是每次操作DB都開啓新連接。

在Spring Boot項目中,默認的DataSource爲com.zaxxer.hikari.HikariDataSource,如果不進行任何配置,那麼Spring Boot將採用HikariDataSource連接池。

我一般用阿里的DruidDataSource,性能比較高,而且功能豐富,支持SQL語句執行監控,可以快速定位到項目中的慢查詢,然後針對慢查詢SQL進行優化。

2、Mybatis獲取連接的流程

Debug跟了一下MyBatis獲取MySQL連接的流程,大致如下:

  1. org.apache.ibatis.executor.BaseExecutor#getConnection()獲取連接。
  2. org.mybatis.spring.transaction.SpringManagedTransaction#getConnection()獲取連接。
  3. SpringManagedTransaction內部的DataSource就是yml裏配置的DataSource。
  4. org.springframework.jdbc.datasource.DataSourceUtils#getConnection(DataSource dataSource)。
  5. org.springframework.jdbc.datasource.DataSourceUtils#fetchConnection(DataSource dataSource)。
  6. 最終在DataSourceUtils類中通過fetchConnection()獲取連接:dataSource.getConnection()。

3、手寫連接池

爲了便於更好的理解數據庫連接池的思想,於是決定自己手寫實現一下。

MyBatis獲取連接,就是通過DataSourceUtils來獲取的,內部就是調用了dataSource.getConnection()
因此,我們只需要DataSource接口,重寫getConnection()方法即可。

需要注意的是:僅實現DataSource接口是不夠的,還需要重寫Connection的close()邏輯,默認邏輯是關閉連接,但是連接池的close()邏輯應該是將連接歸還到池子。

重寫Connection的close()邏輯方式有很多種,可以創建一個Connection的包裝類,但是需要重寫的方法太多了,比較麻煩,於是我這裏使用JDK的動態代理來實現。

3.1、實現DataSource

public class MyDataSourcePool implements DataSource {
	//連接池容量
	private static final int POOL_SIZE = 10;
	//連接隊列
	private final LinkedBlockingQueue<Connection> connectionQueue;

	public MyDataSourcePool() throws SQLException {
		//創建連接,備用
		connectionQueue = new LinkedBlockingQueue<>(POOL_SIZE);
		for (int i = 0; i < POOL_SIZE; i++) {
			Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "root");
			connectionQueue.add(new ConnectionProxy(connection, this).getProxy());
		}
	}

	@Override
	public Connection getConnection() throws SQLException {
		try {
			//10s拿不到連接,就超時
			return connectionQueue.poll(10, TimeUnit.SECONDS);
		} catch (InterruptedException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}

	//釋放連接到連接池
	public void release(Connection connection){
		this.connectionQueue.add(connection);
	}
}

3.2、動態代理Connection

public class ConnectionProxy implements InvocationHandler {
	private Connection connection;
	private MyDataSourcePool myDataSourcePool;
	private Connection proxy;

	public ConnectionProxy(Connection connection,MyDataSourcePool myDataSourcePool) {
		this.connection = connection;
		this.myDataSourcePool = myDataSourcePool;
	}

	public Connection getProxy() {
		proxy = (Connection) Proxy.newProxyInstance(connection.getClass().getClassLoader(), connection.getClass().getInterfaces(), this);
		return proxy;
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		Object result = null;
		if ("close".equals(method.getName())) {
			//修改close()的邏輯
			myDataSourcePool.release(this.proxy);
		}else {
			//其他方法不動
			result = method.invoke(connection, args);
		}
		return result;
	}
}

3.3、配置連接池

server:
  port: 8001
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/test
    username: root
    password: root
    type: com.xw.pool.MyDataSourcePool
    driver-class-name: com.mysql.cj.jdbc.Driver

每次要執行SQL前,都會通過DataSourceUtils.fetchConnection()獲取連接,SQL執行完畢後,會調用DataSourceUtils.doCloseConnection()來關閉連接。

連接池所要做的,就是實現DataSource.getConnection()Connection.close()邏輯。

4、性能測試

手寫的數據庫連接池功能相對簡單,所以基本不會有額外的開銷影響性能。
筆者和阿里的DruidDataSource進行了簡單的性能比較。

連接池默認10個連接,JMeter開啓50個線程進行壓測30秒,結果如下:

  • DruidDataSource
    在這裏插入圖片描述

  • 自己手寫的
    在這裏插入圖片描述

性能差別不大,筆記本環境比較複雜,不是專門的服務器測試,小波動可以忽略不計。
DruidDataSource的峯值在10000,自己寫的在11000左右,因爲DruidDataSource還做了一些例如:防止SQL攻擊,SQL耗時統計等其他工作,最終結果意料之中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章