第一節 從零開始手寫 mybatis(一)MVP 版本 中咱們實現了一個最基本的能夠運行的 mybatis。java
第二節 從零開始手寫 mybatis(二)mybatis interceptor 插件機制詳解mysql
本節咱們一塊兒來看一下如何實現一個數據庫鏈接池。git
數據庫鏈接的建立是很是耗時的一個操做,在高併發的場景,若是每次對於數據庫的訪問都從新建立的話,成本過高。github
因而就有了「池化」這種解決方案。sql
這種方案在咱們平常生活中也是比比皆是,好比資金池,需求池,乃至人力資源池。數據庫
思想都是共通的。apache
咱們本節一塊兒來從零實現一個簡易版本的數據庫鏈接池,不過麻雀雖小,五臟俱全。tomcat
將從如下幾個方面來展開:安全
(1)普通的數據庫鏈接建立mybatis
(2)自動適配 jdbc 驅動
(3)指定大小的鏈接池建立
(4)獲取鏈接時添加超時檢測
(5)添加對於鏈接有效性的檢測
這種就是最普通的不適用池化的實現。
mybatis 默認其實也是這種實現,不過咱們在這個基礎上作了一點優化,那就是能夠根據 url 自動適配 driverClass。
public class UnPooledDataSource extends AbstractDataSourceConfig { @Override public Connection getConnection() throws SQLException { DriverClassUtil.loadDriverClass(super.driverClass, super.jdbcUrl); return DriverManager.getConnection(super.getJdbcUrl(), super.getUser(), super.getPassword()); } }
這個特性主要是參考阿里的 druid 鏈接池實現,在用戶沒有指定驅動類時,自動適配。
核心代碼以下:
/** * 加載驅動類信息 * @param driverClass 驅動類 * @param url 鏈接信息 * @since 1.2.0 */ public static void loadDriverClass(String driverClass, final String url) { ArgUtil.notEmpty(url, url); if(StringUtil.isEmptyTrim(driverClass)) { driverClass = getDriverClassByUrl(url); } try { Class.forName(driverClass); } catch (ClassNotFoundException e) { throw new JdbcPoolException(e); } }
如何根據 url 獲取啓動類呢?實際上就是一個 map 映射。
/** * 根據 URL 獲取對應的驅動類 * * 1. 禁止 url 爲空 * 2. 若是未找到,則直接報錯。 * @param url url * @return 驅動信息 */ private static String getDriverClassByUrl(final String url) { ArgUtil.notEmpty(url, "url"); for(Map.Entry<String, String> entry : DRIVER_CLASS_MAP.entrySet()) { String urlPrefix = entry.getKey(); if(url.startsWith(urlPrefix)) { return entry.getValue(); } } throw new JdbcPoolException("Can't auto find match driver class for url: " + url); }
其中 DRIVER_CLASS_MAP 映射以下:
url 前綴 | 驅動類 |
---|---|
jdbc:sqlite | org.sqlite.JDBC |
jdbc:derby | org.apache.derby.jdbc.EmbeddedDriver |
jdbc:edbc | ca.edbc.jdbc.EdbcDriver |
jdbc:ingres | com.ingres.jdbc.IngresDriver |
jdbc:hsqldb | org.hsqldb.jdbcDriver |
jdbc:JSQLConnect | com.jnetdirect.jsql.JSQLDriver |
jdbc:sybase:Tds | com.sybase.jdbc2.jdbc.SybDriver |
jdbc:firebirdsql | org.firebirdsql.jdbc.FBDriver |
jdbc:microsoft | com.microsoft.jdbc.sqlserver.SQLServerDriver |
jdbc:mckoi | com.mckoi.JDBCDriver |
jdbc:oracle | oracle.jdbc.driver.OracleDriver |
jdbc:as400 | com.ibm.as400.access.AS400JDBCDriver |
jdbc:fake | com.alibaba.druid.mock.MockDriver |
jdbc:pointbase | com.pointbase.jdbc.jdbcUniversalDriver |
jdbc:sapdb | com.sap.dbtech.jdbc.DriverSapDB |
jdbc:postgresql | org.postgresql.Driver |
jdbc:cloudscape | COM.cloudscape.core.JDBCDriver |
jdbc:timesten | com.timesten.jdbc.TimesTenDriver |
jdbc:h2 | org.h2.Driver |
jdbc:jtds | net.sourceforge.jtds.jdbc.Driver |
jdbc:odps | com.aliyun.odps.jdbc.OdpsDriver |
jdbc:db2 | COM.ibm.db2.jdbc.app.DB2Driver |
jdbc:mysql | com.mysql.jdbc.Driver |
jdbc:informix-sqli | com.informix.jdbc.IfxDriver |
jdbc:mock | com.alibaba.druid.mock.MockDriver |
jdbc:mimer:multi1 | com.mimer.jdbc.Driver |
jdbc:interbase | interbase.interclient.Driver |
jdbc:JTurbo | com.newatlanta.jturbo.driver.Driver |
接下來咱們根據指定的大小建立一個初始化的鏈接池。
咱們首先定義一個接口:
/** * 池化的鏈接池 * @since 1.1.0 */ public interface IPooledConnection extends Connection { /** * 是否繁忙 * @since 1.1.0 * @return 狀態 */ boolean isBusy(); /** * 設置狀態 * @param busy 狀態 * @since 1.1.0 */ void setBusy(boolean busy); /** * 獲取真正的鏈接 * @return 鏈接 * @since 1.1.0 */ Connection getConnection(); /** * 設置鏈接信息 * @param connection 鏈接信息 * @since 1.1.0 */ void setConnection(Connection connection); /** * 設置對應的數據源 * @param dataSource 數據源 * @since 1.5.0 */ void setDataSource(final IPooledDataSourceConfig dataSource); /** * 獲取對應的數據源信息 * @return 數據源 * @since 1.5.0 */ IPooledDataSourceConfig getDataSource(); }
這裏咱們直接繼承了 Connection 接口,實現時所有對 Connection 作一個代理。
內容較多,可是比較簡單,此處再也不贅述。
根據配置初始化大小:
/** * 初始化鏈接池 * @since 1.1.0 */ private void initJdbcPool() { final int minSize = super.minSize; pool = new ArrayList<>(minSize); for(int i = 0; i < minSize; i++) { IPooledConnection pooledConnection = createPooledConnection(); pool.add(pooledConnection); } }
createPooledConnection 內容以下:
/** * 建立一個池化的鏈接 * @return 鏈接 * @since 1.1.0 */ private IPooledConnection createPooledConnection() { Connection connection = createConnection(); IPooledConnection pooledConnection = new PooledConnection(); pooledConnection.setBusy(false); pooledConnection.setConnection(connection); pooledConnection.setDataSource(this); return pooledConnection; }
咱們使用 busy 屬性,來標識當前鏈接是否可用。
新建立的鏈接默認都是可用的。
總體流程以下:
(1)池中有鏈接,直接獲取
(2)池中沒有鏈接,且沒達到最大的大小,能夠建立一個,而後返回
(3)池中沒有鏈接,可是已經達到最大,則進行等待。
@Override public synchronized Connection getConnection() throws SQLException { //1. 獲取第一個不是 busy 的鏈接 Optional<IPooledConnection> connectionOptional = getFreeConnectionFromPool(); if(connectionOptional.isPresent()) { return connectionOptional.get(); } //2. 考慮是否能夠擴容 if(pool.size() >= maxSize) { //2.1 馬上返回 if(maxWaitMills <= 0) { throw new JdbcPoolException("Can't get connection from pool!"); } //2.2 循環等待 final long startWaitMills = System.currentTimeMillis(); final long endWaitMills = startWaitMills + maxWaitMills; while (System.currentTimeMillis() < endWaitMills) { Optional<IPooledConnection> optional = getFreeConnectionFromPool(); if(optional.isPresent()) { return optional.get(); } DateUtil.sleep(1); LOG.debug("等待鏈接池歸還,wait for 1 mills"); } //2.3 等待超時 throw new JdbcPoolException("Can't get connection from pool, wait time out for mills: " + maxWaitMills); } //3. 擴容(暫時只擴容一個) LOG.debug("開始擴容鏈接池大小,step: 1"); IPooledConnection pooledConnection = createPooledConnection(); pooledConnection.setBusy(true); this.pool.add(pooledConnection); LOG.debug("從擴容後的鏈接池中獲取鏈接"); return pooledConnection; }
getFreeConnectionFromPool() 核心代碼以下:
直接獲取一個不是繁忙狀態的鏈接便可。
/** * 獲取空閒的鏈接 * @return 鏈接 * @since 1.3.0 */ private Optional<IPooledConnection> getFreeConnectionFromPool() { for(IPooledConnection pc : pool) { if(!pc.isBusy()) { pc.setBusy(true); LOG.debug("從鏈接池中獲取鏈接"); return Optional.of(pc); } } // 空 return Optional.empty(); }
之前 connection 的歸仍是直接將鏈接關閉,這裏咱們作了一個重載。
只是調整下對應的狀態便可。
@Override public void returnConnection(IPooledConnection pooledConnection) { // 驗證狀態 if(testOnReturn) { checkValid(pooledConnection); } // 設置爲不繁忙 pooledConnection.setBusy(false); LOG.debug("歸還鏈接,狀態設置爲不繁忙"); }
池中的鏈接存在無效的可能,因此須要咱們對其進行按期的檢測。
驗證的時機是一門學問,咱們能夠在獲取時檢測,能夠在歸還時檢測,可是兩者都比較消耗性能。
比較好的方式是在空閒的時候進行校驗。
配置主要參考 druid 的配置,對應的接口以下:
/** * 設置驗證查詢的語句 * * 若是這個值爲空,那麼 {@link #setTestOnBorrow(boolean)} * {@link #setTestOnIdle(boolean)}} * {@link #setTestOnReturn(boolean)} * 都將無效 * @param validQuery 驗證查詢的語句 * @since 1.5.0 */ void setValidQuery(final String validQuery); /** * 驗證的超時秒數 * @param validTimeOutSeconds 驗證的超時秒數 * @since 1.5.0 */ void setValidTimeOutSeconds(final int validTimeOutSeconds); /** * 獲取鏈接時進行校驗 * * 備註:影響性能 * @param testOnBorrow 是否 * @since 1.5.0 */ void setTestOnBorrow(final boolean testOnBorrow); /** * 歸還鏈接時進行校驗 * * 備註:影響性能 * @param testOnReturn 歸還鏈接時進行校驗 * @since 1.5.0 */ void setTestOnReturn(final boolean testOnReturn); /** * 閒暇的時候進行校驗 * @param testOnIdle 閒暇的時候進行校驗 * @since 1.5.0 */ void setTestOnIdle(final boolean testOnIdle); /** * 閒暇時進行校驗的時間間隔 * @param testOnIdleIntervalSeconds 時間間隔 * @since 1.5.0 */ void setTestOnIdleIntervalSeconds(final long testOnIdleIntervalSeconds);
全部的屬性都支持用戶自定義,以知足不一樣的應用場景。
同時也秉承着默認的配置就是最經常使用的配置,默認的配置以下:
/** * 默認驗證查詢的語句 * @since 1.5.0 */ public static final String DEFAULT_VALID_QUERY = "select 1 from dual"; /** * 默認的驗證的超時時間 * @since 1.5.0 */ public static final int DEFAULT_VALID_TIME_OUT_SECONDS = 5; /** * 獲取鏈接時,默認不校驗 * @since 1.5.0 */ public static final boolean DEFAULT_TEST_ON_BORROW = false; /** * 歸還鏈接時,默認不校驗 * @since 1.5.0 */ public static final boolean DEFAULT_TEST_ON_RETURN = false; /** * 默認閒暇的時候,進行校驗 * * @since 1.5.0 */ public static final boolean DEFAULT_TEST_ON_IDLE = true; /** * 1min 自動校驗一次 * * @since 1.5.0 */ public static final long DEFAULT_TEST_ON_IDLE_INTERVAL_SECONDS = 60;
這裏我參考了一篇 statckOverflow 的文章,其實仍是使用 Connection#isValid 驗證比較簡單。
/** * https://stackoverflow.com/questions/3668506/efficient-sql-test-query-or-validation-query-that-will-work-across-all-or-most * * 真正支持標準的,直接使用 {@link Connection#isValid(int)} 驗證比較合適 * @param pooledConnection 鏈接池信息 * @since 1.5.0 */ private void checkValid(final IPooledConnection pooledConnection) { if(StringUtil.isNotEmpty(super.validQuery)) { Connection connection = pooledConnection.getConnection(); try { // 若是鏈接無效,從新申請一個新的替代 if(!connection.isValid(super.validTimeOutSeconds)) { LOG.debug("Old connection is inValid, start create one for it."); Connection newConnection = createConnection(); pooledConnection.setConnection(newConnection); LOG.debug("Old connection is inValid, finish create one for it."); } } catch (SQLException throwables) { throw new JdbcPoolException(throwables); } } else { LOG.debug("valid query is empty, ignore valid."); } }
咱們爲了避免影響性能,單獨爲閒暇的鏈接檢測開一個線程。
在初始化的建立:
/** * 初始化空閒時檢驗 * @since 1.5.0 */ private void initTestOnIdle() { if(StringUtil.isNotEmpty(validQuery)) { ScheduledExecutorService idleExecutor = Executors.newSingleThreadScheduledExecutor(); idleExecutor.scheduleAtFixedRate(new Runnable() { @Override public void run() { testOnIdleCheck(); } }, super.testOnIdleIntervalSeconds, testOnIdleIntervalSeconds, TimeUnit.SECONDS); LOG.debug("Test on idle config with interval seonds: " + testOnIdleIntervalSeconds); } }
testOnIdleCheck 實現以下:
/** * 驗證全部的空閒鏈接是否有效 * @since 1.5.0 */ private void testOnIdleCheck() { LOG.debug("start check test on idle"); for(IPooledConnection pc : this.pool) { if(!pc.isBusy()) { checkValid(pc); } } LOG.debug("finish check test on idle"); }
全部源碼均已開源:
使用方式和常見的鏈接池同樣。
<dependency> <groupId>com.github.houbb</groupId> <artifactId>jdbc-pool</artifactId> <version>1.5.0</version> </dependency>
PooledDataSource source = new PooledDataSource(); source.setDriverClass("com.mysql.jdbc.Driver"); source.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8"); source.setUser("root"); source.setPassword("123456"); source.setMinSize(1); // 初始化 source.init(); Connection connection = source.getConnection(); System.out.println(connection.getCatalog()); Connection connection2 = source.getConnection(); System.out.println(connection2.getCatalog());
[DEBUG] [2020-07-18 10:50:54.536] [main] [c.g.h.t.p.d.PooledDataSource.getFreeConnection] - 從鏈接池中獲取鏈接 test [DEBUG] [2020-07-18 10:50:54.537] [main] [c.g.h.t.p.d.PooledDataSource.getConnection] - 開始擴容鏈接池大小,step: 1 [DEBUG] [2020-07-18 10:50:54.548] [main] [c.g.h.t.p.d.PooledDataSource.getConnection] - 從擴容後的鏈接池中獲取鏈接 test
到這裏,一個簡單版本的鏈接池就已經實現了。
常見的鏈接池,好比 dbcp/c3p0/druid/jboss-pool/tomcat-pool 其實都是相似的。
萬變不離其宗,實現只是一種思想的差別化表示而已。
可是有哪些不足呢?
性能方面,咱們爲了簡單,都是直接使用 synchronized
保證併發安全,這樣性能會相對於樂觀鎖,或者是無鎖差一些。
自定義方面,好比 druid 能夠支持用戶自定義攔截器,添加註入防止 sql 注入,耗時統計等等。
頁面管理,druid 比較優異的一點就是自帶頁面管理,這一點對於平常維護也比較友好。