從原始碼分析DBCP資料庫連線池的原理

mayoi7發表於2019-05-31

背景

資料庫連線池的概念大家應該都很熟悉,類似dbcp、c3p0,還有如今常用的druid等,使用時無非就是從網上抄幾段配置,調一下引數,但是深入理解資料庫連線池的原理在如今的Java開發中還是很有必要的,這裡以dbcp為例,簡要分析一下資料庫連線池的實現原理

這裡我採用的版本是2.5.0版本

	<dependency>
	    <groupId>org.apache.commons</groupId>
	    <artifactId>commons-dbcp2</artifactId>
	    <version>2.5.0</version>
	</dependency>
複製程式碼

分析

準備

我們先來看一段原生jdbc程式碼(省略了異常捕獲):

        Class.forName("com.mysql.cj.jdbc.Driver");
        String url = "jdbc:mysql://localhost:3306/xxxxxx";

        Connection conn = DriverManager.getConnection(url, "user", "password");
        Statement stat = conn.createStatement();
        ResultSet set = stat.executeQuery(sql);

        while(set.next()) {
            System.out.println(set.getString(2));
        }

        stat.close();
        conn.close();
複製程式碼

程式碼結構這裡不關心,重點是其中的一句

		Connection conn = DriverManager.getConnection(url, "user", "password");
複製程式碼

為什麼要單獨拿這一條出來說呢?因為我們使用資料庫連線池也是呼叫了getConnection方法,在JDBC 2.0 API中,Java提倡使用DateSource介面來獲取連線

我們現在的重點就是找到dhcp提供的DataSource介面,如果我們使用過dhcp配置,就一定見過這樣的配置

spring:
  datasource:
    type: org.apache.commons.dbcp2.BasicDataSource
複製程式碼

我們就以BasicDataSource作為突破點,這個類實現了DataSource介面:

public class BasicDataSource implements DataSource, BasicDataSourceMXBean, MBeanRegistration, AutoCloseable
複製程式碼

既然我們要探究原理,就不要關注細枝末節的部分,我們直接點進最核心的getConnection方法,同時為了程式碼結構清晰,這裡忽略所有異常捕獲語句:

    @Override
    public Connection getConnection() throws SQLException {
        if (Utils.IS_SECURITY_ENABLED) {
            final PrivilegedExceptionAction<Connection> action = new PaGetConnection();
            return AccessController.doPrivileged(action);
        }
        return createDataSource().getConnection();
    }
複製程式碼

整段程式碼很簡單,根據是否開啟安全管理,來選擇獲取連線的方式,我們這裡只看沒有安全管理的方式

資料來源的建立

我們先來看createDataSource方法:

    protected DataSource createDataSource() throws SQLException {
        if (closed) {
            throw new SQLException("Data source is closed");
        }
        // 如果資料來源已存在,就直接返回
        if (dataSource != null) {
            return dataSource;
        }
        synchronized (this) {
        	// 使用雙檢鎖的設計,保證dataSource是單例的
            if (dataSource != null) {
                return dataSource;
            }

			// 註冊MBean,這裡不是重點,可以暫時不關心
            jmxRegister();

            // 建立返回原生連線的工廠
            final ConnectionFactory driverConnectionFactory = createConnectionFactory();

            // 建立連線池工廠
            boolean success = false;
            PoolableConnectionFactory poolableConnectionFactory;
            try {
                poolableConnectionFactory = createPoolableConnectionFactory(driverConnectionFactory);
                poolableConnectionFactory.setPoolStatements(poolPreparedStatements);
                poolableConnectionFactory.setMaxOpenPreparedStatements(maxOpenPreparedStatements);
                success = true;
            } catch (final SQLException se) {
                throw se;
            } catch (final RuntimeException rte) {
                throw rte;
            } catch (final Exception ex) {
                throw new SQLException("Error creating connection factory", ex);
            }

            if (success) {
                // 建立連線池成功
                createConnectionPool(poolableConnectionFactory);
            }

            // 建立資料來源池來管理連線
            DataSource newDataSource;
            success = false;
            try {
                newDataSource = createDataSourceInstance();
                newDataSource.setLogWriter(logWriter);
                success = true;
            } catch (final SQLException se) {
                throw se;
            } catch (final RuntimeException rte) {
                throw rte;
            } catch (final Exception ex) {
                throw new SQLException("Error creating datasource", ex);
            } finally {
                if (!success) {
                    closeConnectionPool();
                }
            }

            // 如果初始化連線數大於0,就進行預載入
            try {
                for (int i = 0; i < initialSize; i++) {
                    connectionPool.addObject();
                }
            } catch (final Exception e) {
                closeConnectionPool();
                throw new SQLException("Error preloading the connection pool", e);
            }

            // 如果空間連線回收器執行間隔時間大於0,則新增回收器任務
            startPoolMaintenance();

			// 返回資料來源
            dataSource = newDataSource;
            return dataSource;
        }
    }
複製程式碼

程式碼看似很長,其實流程很簡單:

  1. 判斷能不能直接返回結果(資料來源關閉或已存在),如果能,就直接返回
  2. 建立返回資料庫連線的工廠
  3. 建立連線池工廠
  4. 資料庫連線工廠來包裝連線池工廠
  5. 建立資料庫連線池
  6. 通過連線池來建立資料來源例項
  7. 根據設定的引數,判斷是否要進行額外操作(比如預載入資料庫連線)
  8. 返回資料來源

我們重點只有三個:資料庫連線工廠是什麼,連線池工廠是什麼,連線池是什麼。我們先來看資料庫連線工廠

createConnectionFactory

這個方法的程式碼相比於上面的又有些混亂了,為了助於理解,我這裡還是忽略了所有的異常捕獲程式碼段:

    protected ConnectionFactory createConnectionFactory() throws SQLException {
        // 載入JDBC驅動
        Driver driverToUse = this.driver;

        if (driverToUse == null) {
            Class<?> driverFromCCL = null;
            if (driverClassName != null) {
                if (driverClassLoader == null) {
                    driverFromCCL = Class.forName(driverClassName);
                } else {
                    driverFromCCL = Class.forName(driverClassName, true, driverClassLoader);
                }
            }

            if (driverFromCCL == null) {
            	// 這裡的url就是我們與資料庫建立連線的url
                driverToUse = DriverManager.getDriver(url);
            } else {
                driverToUse = (Driver) driverFromCCL.getConstructor().newInstance();
                if (!driverToUse.acceptsURL(url)) {
                    throw new SQLException("No suitable driver", "08001");
                }
            }
        }

        // 設定連線
        final String user = userName;
        if (user != null) {
            connectionProperties.put("user", user);
        } else {
            log("DBCP DataSource configured without a 'username'");
        }

        final String pwd = password;
        if (pwd != null) {
            connectionProperties.put("password", pwd);
        } else {
            log("DBCP DataSource configured without a 'password'");
        }

        final ConnectionFactory driverConnectionFactory = new DriverConnectionFactory(driverToUse, url,
                connectionProperties);
        return driverConnectionFactory;
    }
複製程式碼

這個方法做的事情很簡單,就是單純的載入驅動,真正建立連線工廠的是倒數第二行new DriverConnectionFactory這段程式碼,最終我們建立的也是DriverConnectionFactory型別物件,這裡我們向這個工廠物件傳入了資料庫驅動、連線url、使用者名稱和密碼等屬性

關於DriverConnectionFactory工廠類,這裡就列一段其中的程式碼,我相信其他就不用多講了

    @Override
    public Connection createConnection() throws SQLException {
        return driver.connect(connectionString, properties);
    }
複製程式碼
PoolableConnectionFactory

接下來看這個連線池工廠類,但是我們並不準備先研究這個類,而是回到createDataSource方法中,發現有一個createConnectionPool方法,傳入了PoolableConnectionFactory類物件,我們進入這個方法中:

    protected void createConnectionPool(final PoolableConnectionFactory factory) {
        // 建立一個物件池
        final GenericObjectPoolConfig<PoolableConnection> config = new GenericObjectPoolConfig<>();
        
        updateJmxName(config);
        config.setJmxEnabled(registeredJmxObjectName != null);
        
        final GenericObjectPool<PoolableConnection> gop = createObjectPool(factory, config, abandonedConfig);
        gop.setMaxTotal(maxTotal);
        gop.setMaxIdle(maxIdle);
        gop.setMinIdle(minIdle);
        gop.setMaxWaitMillis(maxWaitMillis);
        gop.setTestOnCreate(testOnCreate);
        gop.setTestOnBorrow(testOnBorrow);
        gop.setTestOnReturn(testOnReturn);
        gop.setNumTestsPerEvictionRun(numTestsPerEvictionRun);
        gop.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        gop.setSoftMinEvictableIdleTimeMillis(softMinEvictableIdleTimeMillis);
        gop.setTestWhileIdle(testWhileIdle);
        gop.setLifo(lifo);
        gop.setSwallowedExceptionListener(new SwallowedExceptionLogger(log, logExpiredConnections));
        gop.setEvictionPolicyClassName(evictionPolicyClassName);
        
        factory.setPool(gop);
        connectionPool = gop;
    }
複製程式碼
  1. 第一行建立了一個物件池,Apache提供了四種物件池,GenericObjectPoolConfig只是其中一種,我們在本篇中暫且不關心其實現細節,只需要明白它是一個物件池就夠了
  2. 接下來兩行與Jmx有關,這裡暫且不關心
  3. 接下來數行,通過我們提供的連線池和其他配置來建立一個物件池,同時設定變數的值,這些值都可以通過我們的配置檔案來設定
  4. 最後我們再把連線池設定到PoolableConnectionFactory工廠類中,並將資料來源本身的連線池設定為該連線池

現在明白我們為什麼不直接研究PoolableConnectionFactory了吧,因為實際上我們呼叫的連線池是connectionPool

獲取連線

剛才我們介紹了了連線工廠、連線池工廠和連線池,為了防止你忘記接下來要幹什麼,這裡再把我們一開始研究的程式碼段放上來:

		return createDataSource().getConnection();
複製程式碼

剛才只是研究了createDataSource方法,這個方法最終返回了一個資料來源物件(這個資料來源物件是通過connectionPool包裝而來的PoolingDataSource<PoolableConnection>物件),我們接下來就要看真正獲取連線的方法getConnection

這是一個抽象方法,因為createDataSource返回的是PoolingDataSource物件,所以我們就從這個物件中來分析,為了讓程式碼結構清晰,這裡依然略去了異常捕獲程式碼:

    @Override
    public Connection getConnection() throws SQLException {
        final C conn = pool.borrowObject();
        if (conn == null) {
            return null;
        }
        return new PoolGuardConnectionWrapper<>(conn);
    }
複製程式碼

我們先解決兩個可能會有的問題:

  • C是Connection子類
  • PoolGuardConnectionWrapper是為了確保被關閉的連線不能被重用的包裝連線類

現在應該就沒有問題了,整個流程就是獲取連線,如果獲取不到就返回空,否則返回一個包裝類。連線通過borrowObject方法來獲取,這個方法的實現是在GenericObjectPool中(不知道為什麼是這個類的一定是前面的程式碼沒有認真看),關於這個類的實現方法就不是本篇的重點,在以後我會單獨寫一篇文章來介紹Apache提供的四種物件池

總結

最後依照慣例,來總結一下DBCP連線池獲取連線的步驟,這裡以呼叫getConnection方法獲取連線為例分析:

  1. 判斷資料來源是否關閉或已存在,如果關閉則丟擲異常,如果已存在則直接返回
  2. 建立可以返回資料庫連線的工廠類,並載入資料庫驅動
  3. 通過資料庫連線工廠類,來建立一個連線池工廠類
  4. 通過連線池工廠類,建立資料庫連線池並設定屬性
  5. 通過連線池來建立資料來源,並根據配置的屬性來進行一些初始化操作
  6. 通過資料來源中的物件池來獲取物件(連線)

相關文章