《Mybatis 手擼專欄》第6章:資料來源池化技術實現

小傅哥發表於2022-04-25

作者:小傅哥
部落格:https://bugstack.cn - 手寫Mybatis系列文章

沉澱、分享、成長,讓自己和他人都能有所收穫!?

一、前言

碼農,只會做不會說?

你有發現嗎,其實很大一部分碼農,都只是會寫程式碼,不會講東西。一遇到述職、答辯、分享、彙報,就很難流暢且有高度、有深度,並融合一部分引入入勝的趣味性來讓觀眾更好的接受和理解你要傳遞的資訊。

那為什麼已經做了還講不出來呢?因為做只是在已確定目標和既定路線下的執行,但為什麼確定這個目標、為什麼制定這個路線、橫向的參照對比、縱向的深度設計,都沒有在一個執行者的頭腦中形成過程的推演,更多的時候都只是執行。所以也就很難把一個事完整的表述出來。

所以,只有當你經歷的足夠多,閱歷的足夠豐富,才能有更好的表述能力和臨場應變技巧,也就更能更清楚的傳遞出你要表達的資訊。

二、目標

在上一章節我們解析了 XML 中資料來源配置資訊,並使用 Druid 建立資料來源完成資料庫的操作。但其實在 Mybatis 中是有自己的資料來源實現的,包括無池化的 UnpooledDataSource 實現方式和有池化的 PooledDataSource 實現方式。

那麼本章節我們就來實現一下關於池化資料來源的處理,通過這些實現讀者也能更好的理解在我們日常開發中一些關於資料來源的配置屬性到底意欲何為,包括:最大活躍連線數、空閒連線數、檢測時長等,在連線池中所起到的作用。

三、設計

首先你可以把池化技術理解為享元模式的具體實現方案,通常我們對一些需要較高建立成本且高頻使用的資源,需要進行快取或者也稱預熱處理。並把這些資源存放到一個預熱池子中,需要用的時候從池子中獲取,使用完畢再進行使用。通過池化可以非常有效的控制資源的使用成本,包括;資源數量、空閒時長、獲取方式等進行統一控制和管理。如圖 6-1 所示

圖 6-1 池化資料來源設計

  • 通過提供統一的連線池中心,存放資料來源連結,並根據配置按照請求獲取連結的操作,建立連線池的資料來源連結數量。這裡就包括了最大空閒連結和最大活躍連結,都隨著建立過程被控制。
  • 此外由於控制了連線池中連線的數量,所以當外部從連線池獲取連結時,如果連結已滿則會進行迴圈等待。這也是大家日常使用DB連線池,如果一個SQL操作引起了慢查詢,則會導致整個服務進入癱瘓的階段,各個和資料庫相關的介面呼叫,都不能獲得到連結,介面查詢TP99陡然增高,系統開始大量報警。那連線池可以配置的很大嗎,也不可以,因為連線池要和資料庫所分配的連線池對應上,避免應用配置連線池超過資料庫所提供的連線池數量,否則會出現夯住不能分配連結的問題,導致資料庫拖垮從而引起連鎖反應。

四、實現

1. 工程結構

mybatis-step-04
└── src
    ├── main
    │   └── java
    │       └── cn.bugstack.mybatis
    │           ├── binding
    │           ├── builder
    │           ├── datasource
    │           │   ├── druid
    │           │   │   └── DruidDataSourceFactory.java
    │           │   ├── pooled
    │           │   │   ├── PooledConnection.java
    │           │   │   ├── PooledDataSource.java
    │           │   │   ├── PooledDataSourceFactory.java
    │           │   │   └── PoolState.java
    │           │   ├── unpooled
    │           │   │   ├── UnpooledDataSource.java
    │           │   │   └── UnpooledDataSourceFactory.java
    │           │   └── DataSourceFactory.java
    │           ├── io
    │           ├── mapping
    │           ├── session
    │           │   ├── defaults
    │           │   │   ├── DefaultSqlSession.java
    │           │   │   └── DefaultSqlSessionFactory.java
    │           │   ├── Configuration.java
    │           │   ├── SqlSession.java
    │           │   ├── SqlSessionFactory.java
    │           │   ├── SqlSessionFactoryBuilder.java
    │           │   └── TransactionIsolationLevel.java  
    │           ├── transaction
    │           └── type
    └── test
        ├── java
        │   └── cn.bugstack.mybatis.test.dao
        │       ├── dao
        │       │   └── IUserDao.java
        │       ├── po
        │       │   └── User.java
        │       └── ApiTest.java
        └── resources
            ├── mapper
            │   └──User_Mapper.xml
            └── mybatis-config-datasource.xml

工程原始碼:https://t.zsxq.com/bmqNFQ7

池化資料來源核心類關係,如圖 6-2 所示

圖 6-2 池化資料來源核心類關係

  • 在 Mybatis 資料來源的實現中,包括兩部分分為無池化的 UnpooledDataSource 實現類和有池化的 PooledDataSource 實現類,池化的實現類 PooledDataSource 以對無池化的 UnpooledDataSource 進行擴充套件處理。把建立出來的連結儲存到記憶體中,記錄為空閒連結和活躍連結,在不同的階段進行使用。
  • PooledConnection 是對連結的代理操作,通過invoke方法的反射呼叫,對關閉的連結進行回收處理,並使用 notifyAll 通知正在等待連結的使用者進行搶連結。
  • 另外是對 DataSourceFactory 資料來源工廠介面的實現,由無池化工廠實現後,有池化工廠繼承的方式進行處理,這裡沒有太多的複雜操作,池化的處理主要集中在 PooledDataSource 類中進行處理。

2. 無池化連結實現

對於資料庫連線池的實現,不一定非得提供池化技術,對於某些場景可以只使用無池化的連線池。那麼在實現的過程中,可以把無池化的實現和池化實現拆分解耦,在需要的時候只需要配置對應的資料來源即可。

public class UnpooledDataSource implements DataSource {

    private ClassLoader driverClassLoader;
    // 驅動配置,也可以擴充套件屬性資訊 driver.encoding=UTF8
    private Properties driverProperties;
    // 驅動註冊器
    private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap<>();
    // 驅動
    private String driver;
    // DB 連結地址
    private String url;
    // 賬號
    private String username;
    // 密碼
    private String password;
    // 是否自動提交
    private Boolean autoCommit;
    // 事務級別
    private Integer defaultTransactionIsolationLevel;

    static {
        Enumeration<Driver> drivers = DriverManager.getDrivers();
        while (drivers.hasMoreElements()) {
            Driver driver = drivers.nextElement();
            registeredDrivers.put(driver.getClass().getName(), driver);
        }
    }

    private Connection doGetConnection(Properties properties) throws SQLException {
        initializerDriver();
        Connection connection = DriverManager.getConnection(url, properties);
        if (autoCommit != null && autoCommit != connection.getAutoCommit()) {
            connection.setAutoCommit(autoCommit);
        }
        if (defaultTransactionIsolationLevel != null) {
            connection.setTransactionIsolation(defaultTransactionIsolationLevel);
        }
        return connection;
    }

    /**
     * 初始化驅動
     */
    private synchronized void initializerDriver() throws SQLException {
        if (!registeredDrivers.containsKey(driver)) {
            try {
                Class<?> driverType = Class.forName(driver, true, driverClassLoader);
                // https://www.kfu.com/~nsayer/Java/dyn-jdbc.html
                Driver driverInstance = (Driver) driverType.newInstance();
                DriverManager.registerDriver(new DriverProxy(driverInstance));
                registeredDrivers.put(driver, driverInstance);
            } catch (Exception e) {
                throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e);
            }
        }
    }
    
}
  • 無池化的資料來源連結實現比較簡單,核心在於 initializerDriver 初始化驅動中使用了 Class.forName 和 newInstance 的方式建立了資料來源連結操作。
  • 在建立完成連線以後,把連結存放到驅動註冊器中,方便後續使用中可以直接獲取連結,避免重複建立所帶來的資源消耗。

3. 有池化連結實現

有池化的資料來源連結,核心在於對無池化連結的包裝,同時提供了相應的池化技術實現,包括:pushConnection、popConnection、forceCloseAll、pingConnection 的操作處理。

這樣當使用者想要獲取連結的時候,則會從連線池中進行獲取,同時判斷是否有空閒連結、最大活躍連結多少,以及是否需要等待處理或是最終丟擲異常。

3.1 池化連線的代理

由於我們需要對連線進行池化處理,所以當連結呼叫一些 CLOSE 方法的時候,也需要把連結從池中關閉和恢復可用,允許其他使用者獲取到連結。那麼這裡就需要對連線類進行代理包裝,處理 CLOSE 方法。

原始碼詳見cn.bugstack.mybatis.datasource.pooled.PooledConnection

public class PooledConnection implements InvocationHandler {

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        // 如果是呼叫 CLOSE 關閉連結方法,則將連結加入連線池中,並返回null
        if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) {
            dataSource.pushConnection(this);
            return null;
        } else {
            if (!Object.class.equals(method.getDeclaringClass())) {
                // 除了toString()方法,其他方法呼叫之前要檢查connection是否還是合法的,不合法要丟擲SQLException
                checkConnection();
            }
            // 其他方法交給connection去呼叫
            return method.invoke(realConnection, args);
        }
    }
    
}
  • 通過 PooledConnection 實現 InvocationHandler#invoke 方法,包裝代理連結,這樣就可以對具體的呼叫方法進行控制了。
  • 在 invoke 方法中處理對 CLOSE 方法控制以外,排除 toString 等Object 的方法後,則是其他真正需要被 DB 連結處理的方法了。
  • 那麼這裡有一個對於 CLOSE 方法的資料來源回收操作 dataSource.pushConnection(this); 有一個具體的實現方法,在池化實現類 PooledDataSource 中進行處理。

3.2 pushConnection 回收連結

原始碼詳見cn.bugstack.mybatis.datasource.pooled.PooledDataSource

protected void pushConnection(PooledConnection connection) throws SQLException {
    synchronized (state) {
        state.activeConnections.remove(connection);
        // 判斷連結是否有效
        if (connection.isValid()) {
            // 如果空閒連結小於設定數量,也就是太少時
            if (state.idleConnections.size() < poolMaximumIdleConnections && connection.getConnectionTypeCode() == expectedConnectionTypeCode) {
                state.accumulatedCheckoutTime += connection.getCheckoutTime();
                if (!connection.getRealConnection().getAutoCommit()) {
                    connection.getRealConnection().rollback();
                }
                // 例項化一個新的DB連線,加入到idle列表
                PooledConnection newConnection = new PooledConnection(connection.getRealConnection(), this);
                state.idleConnections.add(newConnection);
                newConnection.setCreatedTimestamp(connection.getCreatedTimestamp());
                newConnection.setLastUsedTimestamp(connection.getLastUsedTimestamp());
                connection.invalidate();
                logger.info("Returned connection " + newConnection.getRealHashCode() + " to pool.");
                // 通知其他執行緒可以來搶DB連線了
                state.notifyAll();
            }
            // 否則,空閒連結還比較充足
            else {
                state.accumulatedCheckoutTime += connection.getCheckoutTime();
                if (!connection.getRealConnection().getAutoCommit()) {
                    connection.getRealConnection().rollback();
                }
                // 將connection關閉
                connection.getRealConnection().close();
                logger.info("Closed connection " + connection.getRealHashCode() + ".");
                connection.invalidate();
            }
        } else {
            logger.info("A bad connection (" + connection.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
            state.badConnectionCount++;
        }
    }
}
  • 在 PooledDataSource#pushConnection 資料來源回收的處理中,核心在於判斷連結是否有效,以及進行相關的空閒連結校驗,判斷是否把連線回收到 idle 空閒連結列表中,並通知其他執行緒來搶佔。
  • 如果現在空閒連結充足,那麼這個回收的連結則會進行回滾和關閉的處理中。connection.getRealConnection().close();

3.3 popConnection 獲取連結

原始碼詳見cn.bugstack.mybatis.datasource.pooled.PooledDataSource

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()) {
                conn = state.idleConnections.remove(0);
                logger.info("Checked out connection " + conn.getRealHashCode() + " from pool.");
            }
            // 如果無空閒連結:建立新的連結
            else {
                // 活躍連線數不足
                if (state.activeConnections.size() < poolMaximumActiveConnections) {
                    conn = new PooledConnection(dataSource.getConnection(), this);
                    logger.info("Created connection " + conn.getRealHashCode() + ".");
                }
                // 活躍連線數已滿
                else {
                    // 取得活躍連結列表的第一個,也就是最老的一個連線
                    PooledConnection oldestActiveConnection = state.activeConnections.get(0);
                    long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
                    // 如果checkout時間過長,則這個連結標記為過期
                    if (longestCheckoutTime > poolMaximumCheckoutTime) {
                        state.claimedOverdueConnectionCount++;
                        state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
                        state.accumulatedCheckoutTime += longestCheckoutTime;
                        state.activeConnections.remove(oldestActiveConnection);
                        if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
                            oldestActiveConnection.getRealConnection().rollback();
                        }
                        // 刪掉最老的連結,然後重新例項化一個新的連結
                        conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
                        oldestActiveConnection.invalidate();
                        logger.info("Claimed overdue connection " + conn.getRealHashCode() + ".");
                    }
                    // 如果checkout超時時間不夠長,則等待
                    else {
                        try {
                            if (!countedWait) {
                                state.hadToWaitCount++;
                                countedWait = true;
                            }
                            logger.info("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
                            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();
                    }
                    conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
                    // 記錄checkout時間
                    conn.setCheckoutTimestamp(System.currentTimeMillis());
                    conn.setLastUsedTimestamp(System.currentTimeMillis());
                    state.activeConnections.add(conn);
                    state.requestCount++;
                    state.accumulatedRequestTime += System.currentTimeMillis() - t;
                } else {
                    logger.info("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection
                    // 如果沒拿到,統計資訊:失敗連結 +1
                    state.badConnectionCount++;
                    localBadConnectionCount++;
                    conn = null;
                    // 失敗次數較多,拋異常
                    if (localBadConnectionCount > (poolMaximumIdleConnections + 3)) {
                        logger.debug("PooledDataSource: Could not get a good connection to the database.");
                        throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
                    }
                }
            }
        }
    }

    return conn;
}
  • popConnection 獲取連結是一個 while 死迴圈操作,只有獲取到連結拋異常才會退出迴圈,如果仔細閱讀這些異常程式碼,是不是也是你在做一些開發的時候所遇到的異常呢。
  • 獲取連結的過程會使用 synchronized 進行加鎖,因為所有執行緒在資源競爭的情況下,都需要進行加鎖處理。在加鎖的程式碼塊中通過判斷是否還有空閒連結進行返回,如果沒有則會判斷活躍連線數是否充足,不充足則進行建立後返回。在這裡也會遇到活躍連結已經進行迴圈等待的過程,最後再不能獲取則丟擲異常。

4. 資料來源工廠

資料來源工廠包括兩部分,分別是無池化和有池化,有池化的工程繼承無池化工廠,因為在 Mybatis 原始碼的實現類中,這樣就可以減少對 Properties 統一包裝的反射方式的屬性處理。由於我們暫時沒有對這塊邏輯進行開發,只是簡單的獲取屬性傳參,所以還不能體現出這樣的繼承有多便捷,讀者可以參考原始碼進行理解。原始碼類:UnpooledDataSourceFactory

4.1 無池化工廠

原始碼詳見cn.bugstack.mybatis.datasource.unpooled.UnpooledDataSourceFactory

public class UnpooledDataSourceFactory implements DataSourceFactory {

    protected Properties props;

    @Override
    public void setProperties(Properties props) {
        this.props = props;
    }

    @Override
    public DataSource getDataSource() {
        UnpooledDataSource unpooledDataSource = new UnpooledDataSource();
        unpooledDataSource.setDriver(props.getProperty("driver"));
        unpooledDataSource.setUrl(props.getProperty("url"));
        unpooledDataSource.setUsername(props.getProperty("username"));
        unpooledDataSource.setPassword(props.getProperty("password"));
        return unpooledDataSource;
    }

}
  • 簡單包裝 getDataSource 獲取資料來源處理,把必要的引數進行傳遞過去。在 Mybatis 原始碼中這部分則是進行了大量的反射欄位處理的方式進行存放和獲取的。

4.2 有池化工廠

原始碼詳見cn.bugstack.mybatis.datasource.pooled.PooledDataSourceFactory

public class PooledDataSourceFactory extends UnpooledDataSourceFactory {

    @Override
    public DataSource getDataSource() {
        PooledDataSource pooledDataSource = new PooledDataSource();
        pooledDataSource.setDriver(props.getProperty("driver"));
        pooledDataSource.setUrl(props.getProperty("url"));
        pooledDataSource.setUsername(props.getProperty("username"));
        pooledDataSource.setPassword(props.getProperty("password"));
        return pooledDataSource;
    }

}
  • 有池化的資料來源工廠實現的也比較簡單,只是繼承 UnpooledDataSourceFactory 共用獲取屬性的能力,以及例項化出池化資料來源即可。

5. 新增型別別名註冊器

當我們新開發了兩個資料來源和對應的工廠實現類以後,則需要把它們配置到 Configuration 中,這樣才能在解析 XML 時候根據不同的資料來源型別獲取和例項化對應的實現類。

原始碼詳見cn.bugstack.mybatis.session.Configuration

public class Configuration {

    // 型別別名序號產生器
    protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();

    public Configuration() {
        typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);

        typeAliasRegistry.registerAlias("DRUID", DruidDataSourceFactory.class);
        typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
        typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
    }

}
  • 在構造方法 Configuration 新增 UNPOOLED、POOLED 兩個資料來源註冊到型別註冊器中,方便後續使用 XMLConfigBuilder#environmentsElement 方法解析 XML 處理資料來源時候進行使用。

五、測試

1. 事先準備

1.1 建立庫表

建立一個資料庫名稱為 mybatis 並在庫中建立表 user 以及新增測試資料,如下:

CREATE TABLE
    USER
    (
        id bigint NOT NULL AUTO_INCREMENT COMMENT '自增ID',
        userId VARCHAR(9) COMMENT '使用者ID',
        userHead VARCHAR(16) COMMENT '使用者頭像',
        createTime TIMESTAMP NULL COMMENT '建立時間',
        updateTime TIMESTAMP NULL COMMENT '更新時間',
        userName VARCHAR(64),
        PRIMARY KEY (id)
    )
    ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
insert into user (id, userId, userHead, createTime, updateTime, userName) values (1, '10001', '1_04', '2022-04-13 00:00:00', '2022-04-13 00:00:00', '小傅哥');    

1.2 配置資料來源

<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"/>
        <dataSource type="DRUID">
            <property name="driver" value="com.mysql.jdbc.Driver"/>
            <property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true"/>
            <property name="username" value="root"/>
            <property name="password" value="123456"/>
        </dataSource>
    </environment>
</environments>
  • 通過 mybatis-config-datasource.xml 配置資料來源資訊,包括:driver、url、username、password
  • 在這裡 dataSource 的配置又上一章節的 DRUID 修改為,UNPOOLED 和 POOLED 進行測試驗證。這兩個資料來源也就是我們本章節自己實現的資料來源。

1.3 配置Mapper

<select id="queryUserInfoById" parameterType="java.lang.Long" resultType="cn.bugstack.mybatis.test.po.User">
    SELECT id, userId, userName, userHead
    FROM user
    where id = #{id}
</select>
  • Mapper 的配置內容在上一章節的解析學習中已經做了配置,本章節做了簡單的調整。

2. 單元測試

@Test
public void test_SqlSessionFactory() throws IOException {
    // 1. 從SqlSessionFactory中獲取SqlSession
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config-datasource.xml"));
    SqlSession sqlSession = sqlSessionFactory.openSession();
    
    // 2. 獲取對映器物件
    IUserDao userDao = sqlSession.getMapper(IUserDao.class);
    
    // 3. 測試驗證
    for (int i = 0; i < 50; i++) {
        User user = userDao.queryUserInfoById(1L);
        logger.info("測試結果:{}", JSON.toJSONString(user));
    }
}
  • 在無池化和有池化的測試中,基礎的單元測試類不需要改變,仍是通過 SqlSessionFactory 中獲取 SqlSession 並獲得對映物件和執行方法呼叫。另外這裡是新增了50次的查詢呼叫,便於驗證連線池的建立和獲取以及等待。
  • 變化的在於 mybatis-config-datasource.xml 中 dataSource 資料來源型別的調整 dataSource type="POOLED/UNPOOLED"

2.1 無池化測試

<dataSource type="UNPOOLED"></dataSource>

測試結果

11:27:48.604 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 測試結果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
11:27:48.618 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 測試結果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
11:27:48.622 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 測試結果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
11:27:48.632 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 測試結果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
11:27:48.637 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 測試結果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
11:27:48.642 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 測試結果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
11:27:48.649 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 測試結果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
...
  • 無池化的連線池操作,會不斷的與資料庫建立新的連結並執行 SQL 操作,這個過程中只要資料庫還有連結可以被連結,就可以建立連結。

2.2 有池化測試

<dataSource type="POOLED"></dataSource>

測試結果

11:30:22.536 [main] INFO  c.b.m.d.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
11:30:22.541 [main] INFO  c.b.m.d.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
11:30:22.541 [main] INFO  c.b.m.d.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
11:30:22.541 [main] INFO  c.b.m.d.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections.
11:30:22.860 [main] INFO  c.b.m.d.pooled.PooledDataSource - Created connection 540642172.
11:30:22.996 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 測試結果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
11:30:23.009 [main] INFO  c.b.m.d.pooled.PooledDataSource - Created connection 140799417.
11:30:23.011 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 測試結果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
11:30:23.018 [main] INFO  c.b.m.d.pooled.PooledDataSource - Created connection 110431793.
11:30:23.019 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 測試結果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
11:30:23.032 [main] INFO  c.b.m.d.pooled.PooledDataSource - Created connection 1053631449.
11:30:23.033 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 測試結果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
11:30:23.041 [main] INFO  c.b.m.d.pooled.PooledDataSource - Created connection 1693847660.
11:30:23.042 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 測試結果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
11:30:23.047 [main] INFO  c.b.m.d.pooled.PooledDataSource - Created connection 212921632.
11:30:23.048 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 測試結果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
11:30:23.055 [main] INFO  c.b.m.d.pooled.PooledDataSource - Created connection 682376643.
11:30:23.056 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 測試結果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
11:30:23.060 [main] INFO  c.b.m.d.pooled.PooledDataSource - Created connection 334203599.
11:30:23.062 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 測試結果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
11:30:23.067 [main] INFO  c.b.m.d.pooled.PooledDataSource - Created connection 1971851377.
11:30:23.068 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 測試結果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
11:30:23.073 [main] INFO  c.b.m.d.pooled.PooledDataSource - Created connection 399534175.
11:30:23.074 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 測試結果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}
11:30:23.074 [main] INFO  c.b.m.d.pooled.PooledDataSource - Waiting as long as 20000 milliseconds for connection.
11:30:43.078 [main] INFO  c.b.m.d.pooled.PooledDataSource - Claimed overdue connection 540642172.
11:30:43.079 [main] INFO  cn.bugstack.mybatis.test.ApiTest - 測試結果:{"id":1,"userHead":"1_04","userId":"10001","userName":"小傅哥"}

...
  • 通過使用連線池的配置可以看到,在呼叫和獲取連線的過程中,當呼叫次數打到10次以後,連線池中就有了10個活躍的連結,再呼叫的時候則需要等待連線釋放後才能使用並執行 SQL 操作。
  • 測試的過程中還包括了連線的空閒數量、活躍數量、關閉、異常等,讀者夥伴也可以在學習的過程中進行驗證處理。

六、總結

  • 本章節我們完成了 Mybatis 資料來源池化的設計和實現,也能通過這樣的分析、實現、驗證的過程讓大家更好的理解我們平常使用的連線池所遇到的一些真實問題都是怎麼發生的,做到知其然知其所以然。
  • 另外關於連線池的實現重點可以隨著除錯驗證的過程中進行學習,包括:synchronized 加鎖、建立連線、活躍數量控制、休眠等待時長,拋異常邏輯等,這些都與我們日常使用連線池時的配置息息相關。
  • 這一章節的內容可以算作是 Mybatis 核心功能實現過程上的重要分支,雖然可以使用 Druid 替代資料來源的處理,但只有動手自己實現一遍資料來源連線池才能更好的理解池化技術的落地方案,也能為以後做此類功能時,有一個可落地的具體方案。

相關文章