源碼詳解系列(四) ------ DBCP2的使用和分析(包括JNDI和JTA支持)

簡介

DBCP用於建立和管理鏈接,利用「池」的方式複用鏈接減小資源開銷,和其餘鏈接池同樣,也具備鏈接數控制、鏈接有效性檢測、鏈接泄露控制、緩存語句等功能。目前,tomcat自帶的鏈接池就是DBCP,Spring開發組也推薦使用DBCP,阿里的druid也是參照DBCP開發出來的。html

DBCP除了咱們熟知的使用方式外,還支持經過JNDI獲取數據源,並支持獲取JTAXA事務中用於2PC(兩階段提交)的鏈接對象,本文也將以例子說明。java

本文將包含如下內容(由於篇幅較長,可根據須要選擇閱讀):mysql

  1. DBCP的使用方法(入門案例說明);
  2. DBCP的配置參數詳解;
  3. DBCP主要源碼分析;
  4. DBCP其餘特性的使用方法,如JNDIJTA支持。

使用例子

需求

使用DBCP鏈接池獲取鏈接對象,對用戶數據進行簡單的增刪改查。git

工程環境

JDK:1.8.0_201github

maven:3.6.1web

IDE:eclipse 4.12spring

mysql-connector-java:8.0.15sql

mysql:5.7.28數據庫

DBCP:2.6.0apache

主要步驟

  1. 編寫dbcp.properties,設置數據庫鏈接參數和鏈接池基本參數等。

  2. 經過BasicDataSourceFactory加載dbcp.properties,並得到BasicDataDource對象。

  3. 經過BasicDataDource對象獲取Connection對象。

  4. 使用Connection對象對用戶表進行增刪改查。

建立項目

項目類型Maven Project,打包方式war(其實jar也能夠,之因此使用war是爲了測試JNDI)。

引入依賴

<!-- junit -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<!-- dbcp -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-dbcp2</artifactId>
    <version>2.6.0</version>
</dependency>
<!-- log4j -->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
<!-- mysql驅動的jar包 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.15</version>
</dependency>

編寫dbcp.prperties

路徑resources目錄下,由於是入門例子,這裏僅給出數據庫鏈接參數和鏈接池基本參數,後面源碼會對配置參數進行詳細說明。另外,數據庫sql腳本也在該目錄下。

#鏈接基本屬性
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true
username=root
password=root

#-------------鏈接池大小和鏈接超時參數--------------------------------
#初始化鏈接數量:鏈接池啓動時建立的初始化鏈接數量
#默認爲0
initialSize=0

#最大活動鏈接數量:鏈接池在同一時間可以分配的最大活動鏈接的數量, 若是設置爲負數則表示不限制
#默認爲8
maxTotal=8

#最大空閒鏈接:鏈接池中允許保持空閒狀態的最大鏈接數量,超過的空閒鏈接將被釋放,若是設置爲負數表示不限制
#默認爲8
maxIdle=8

#最小空閒鏈接:鏈接池中允許保持空閒狀態的最小鏈接數量,低於這個數量將建立新的鏈接,若是設置爲0則不建立
#注意:timeBetweenEvictionRunsMillis爲正數時,這個參數才能生效。
#默認爲0
minIdle=0

#最大等待時間
#當沒有可用鏈接時,鏈接池等待鏈接被歸還的最大時間(以毫秒計數),超過期間則拋出異常,若是設置爲<=0表示無限等待
#默認-1
maxWaitMillis=-1

獲取鏈接池和獲取鏈接

項目中編寫了JDBCUtils來初始化鏈接池、獲取鏈接、管理事務和釋放資源等,具體參見項目源碼。

路徑:cn.zzs.dbcp

// 導入配置文件
    Properties properties = new Properties();
    InputStream in = JDBCUtil.class.getClassLoader().getResourceAsStream("dbcp.properties");
    properties.load(in);
    // 根據配置文件內容得到數據源對象
    DataSource dataSource = BasicDataSourceFactory.createDataSource(properties);
    // 得到鏈接
    Connection conn = dataSource.getConnection();

編寫測試類

這裏以保存用戶爲例,路徑test目錄下的cn.zzs.dbcp

@Test
    public void save() {
        // 建立sql
        String sql = "insert into demo_user values(null,?,?,?,?,?)";
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            // 得到鏈接
            connection = JDBCUtils.getConnection();
            // 開啓事務設置非自動提交
            JDBCUtils.startTrasaction();
            // 得到Statement對象
            statement = connection.prepareStatement(sql);
            // 設置參數
            statement.setString(1, "zzf003");
            statement.setInt(2, 18);
            statement.setDate(3, new Date(System.currentTimeMillis()));
            statement.setDate(4, new Date(System.currentTimeMillis()));
            statement.setBoolean(5, false);
            // 執行
            statement.executeUpdate();
            // 提交事務
            JDBCUtils.commit();
        } catch(Exception e) {
            JDBCUtils.rollback();
            log.error("保存用戶失敗", e);
        } finally {
            // 釋放資源
            JDBCUtils.release(connection, statement, null);
        }
    }

配置文件詳解

這部份內容從網上參照過來,一樣的內容發的處處都是,暫時沒找到出處。由於內容太過雜亂,並且最新版本更新了很多內容,因此我花了好大功夫才改好,後面找到出處再補上參考資料吧。

基本鏈接屬性

注意,這裏在url後面拼接了多個參數用於避免亂碼、時區報錯問題。 補充下,若是不想加入時區的參數,能夠在mysql命令窗口執行以下命令:set global time_zone='+8:00'

driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true
username=root
password=root

鏈接池大小參數

這幾個參數都比較經常使用,具體設置多少需根據項目調整。

#-------------鏈接池大小和鏈接超時參數--------------------------------
#初始化鏈接數量:鏈接池啓動時建立的初始化鏈接數量
#默認爲0
initialSize=0

#最大活動鏈接數量:鏈接池在同一時間可以分配的最大活動鏈接的數量, 若是設置爲負數則表示不限制
#默認爲8
maxTotal=8

#最大空閒鏈接:鏈接池中允許保持空閒狀態的最大鏈接數量,超過的空閒鏈接將被釋放,若是設置爲負數表示不限制
#默認爲8
maxIdle=8

#最小空閒鏈接:鏈接池中允許保持空閒狀態的最小鏈接數量,低於這個數量將建立新的鏈接,若是設置爲0則不建立
#注意:timeBetweenEvictionRunsMillis爲正數時,這個參數才能生效。
#默認爲0
minIdle=0

#最大等待時間
#當沒有可用鏈接時,鏈接池等待鏈接被歸還的最大時間(以毫秒計數),超過期間則拋出異常,若是設置爲<=0表示無限等待
#默認-1
maxWaitMillis=-1

#鏈接池建立的鏈接的默認的數據庫名,若是是使用DBCP的XA鏈接必須設置,否則註冊不了多個資源管理器
#defaultCatalog=github_demo

#鏈接池建立的鏈接的默認的schema。若是是mysql,這個設置沒什麼用。
#defaultSchema=github_demo

緩存語句

緩存語句在mysql下建議關閉。

#-------------緩存語句--------------------------------
#是否緩存preparedStatement,也就是PSCache。
#PSCache對支持遊標的數據庫性能提高巨大,好比說oracle。在mysql下建議關閉
#默認爲false
poolPreparedStatements=false

#緩存PreparedStatements的最大個數
#默認爲-1
#注意:poolPreparedStatements爲true時,這個參數纔有效
maxOpenPreparedStatements=-1

#緩存read-only和auto-commit狀態。設置爲true的話,全部鏈接的狀態都會是同樣的。
#默認是true
cacheState=true

鏈接檢查參數

針對鏈接失效和鏈接泄露的問題,建議開啓testWhileIdle,而不是開啓testOnReturntestOnBorrow(從性能考慮)。

#-------------鏈接檢查狀況--------------------------------
#經過SQL查詢檢測鏈接,注意必須返回至少一行記錄
#默認爲空。即會調用Connection的isValid和isClosed進行檢測
#注意:若是是oracle數據庫的話,應該改成select 1 from dual
validationQuery=select 1 from dual

#SQL檢驗超時時間
validationQueryTimeout=-1

#是否從池中取出鏈接前進行檢驗。
#默認爲true
testOnBorrow=true

#是否在歸還到池中前進行檢驗 
#默認爲false
testOnReturn=false

#是否開啓空閒資源回收器。
#默認爲false
testWhileIdle=false

#空閒資源的檢測週期(單位爲毫秒)。
#默認-1。即空閒資源回收器不工做。
timeBetweenEvictionRunsMillis=-1

#作空閒資源回收器時,每次的採樣數。
#默認3,單位毫秒。若是設置爲-1,就是對全部鏈接作空閒監測。
numTestsPerEvictionRun=3

#資源池中資源最小空閒時間(單位爲毫秒),達到此值後將被移除。
#默認值1000*60*30 = 30分鐘
minEvictableIdleTimeMillis=1800000

#資源池中資源最小空閒時間(單位爲毫秒),達到此值後將被移除。可是會保證minIdle
#默認值-1
#softMinEvictableIdleTimeMillis=-1

#空閒資源回收策略
#默認org.apache.commons.pool2.impl.DefaultEvictionPolicy
#若是要自定義的話,須要實現EvictionPolicy重寫evict方法
evictionPolicyClassName=org.apache.commons.pool2.impl.DefaultEvictionPolicy

#鏈接最大存活時間。非正數表示不限制
#默認-1
maxConnLifetimeMillis=-1

#當達到maxConnLifetimeMillis被關閉時,是否打印相關消息
#默認true
#注意:maxConnLifetimeMillis設置爲正數時,這個參數纔有效
logExpiredConnections=true

事務相關參數

這裏的參數主要和事務相關,通常默認就行。

#-------------事務相關的屬性--------------------------------
#鏈接池建立的鏈接的默認的auto-commit狀態
#默認爲空,由驅動決定
defaultAutoCommit=true

#鏈接池建立的鏈接的默認的read-only狀態。
#默認值爲空,由驅動決定
defaultReadOnly=false

#鏈接池建立的鏈接的默認的TransactionIsolation狀態
#可用值爲下列之一:NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
#默認值爲空,由驅動決定
defaultTransactionIsolation=REPEATABLE_READ

#歸還鏈接時是否設置自動提交爲true
#默認true
autoCommitOnReturn=true

#歸還鏈接時是否設置回滾事務
#默認true
rollbackOnReturn=true

鏈接泄漏回收參數

當咱們從鏈接池得到了鏈接對象,但由於疏忽或其餘緣由沒有close,這個時候這個鏈接對象就是一個泄露資源。經過配置如下參數能夠回收這部分對象。

#-------------鏈接泄漏回收參數--------------------------------
#當未使用的時間超過removeAbandonedTimeout時,是否視該鏈接爲泄露鏈接並刪除(當getConnection()被調用時檢測)
#默認爲false
#注意:這個機制在(getNumIdle() < 2) and (getNumActive() > (getMaxActive() - 3))時被觸發
removeAbandonedOnBorrow=false

#當未使用的時間超過removeAbandonedTimeout時,是否視該鏈接爲泄露鏈接並刪除(空閒evictor檢測)
#默認爲false
#注意:當空閒資源回收器開啓才生效
removeAbandonedOnMaintenance=false

#泄露的鏈接能夠被刪除的超時值, 單位秒
#默認爲300
removeAbandonedTimeout=300

#標記當Statement或鏈接被泄露時是否打印程序的stack traces日誌。
#默認爲false
logAbandoned=true

#這個不是很懂
#默認爲false
abandonedUsageTracking=false

其餘

這部分參數比較少用。

#-------------其餘--------------------------------
#是否使用快速失敗機制
#默認爲空,由驅動決定
fastFailValidation=false

#當使用快速失敗機制時,設置觸發的異常碼
#多個code用","隔開
#disconnectionSqlCodes

#borrow鏈接的順序
#默認true
lifo=true

#每一個鏈接建立時執行的語句
#connectionInitSqls=

#鏈接參數:例如username、password、characterEncoding等均可以在這裏設置
#多個參數用";"隔開
#connectionProperties=

#指定數據源的jmx名。注意,配置了才能註冊MBean
jmxName=cn.zzs.jmx:type=BasicDataSource,name=zzs001

#查詢超時時間
#默認爲空,即根據驅動設置
#defaultQueryTimeout=

#控制PoolGuard是否允許獲取底層鏈接
#默認爲false
accessToUnderlyingConnectionAllowed=false

#若是允許則可使用下面的方式來獲取底層物理鏈接:
#    Connection conn = ds.getConnection();
#    Connection dconn = ((DelegatingConnection) conn).getInnermostDelegate();
#    ...
#    conn.close();

源碼分析

注意:考慮篇幅和可讀性,如下代碼通過刪減,僅保留所需部分。

建立數據源和鏈接池

研究以前,先來看下BasicDataSourceUML圖:

BasicDataSource的UML圖

這裏介紹下這幾個類的做用:

類名 描述
BasicDataSource 用於知足基本數據庫操做需求的數據源
BasicManagedDataSource BasicDataSource的子類,用於建立支持XA事務或JTA事務的鏈接
PoolingDataSource BasicDataSource中實際調用的數據源,能夠說BasicDataSource只是封裝了PoolingDataSource
ManagedDataSource PoolingDataSource的子類,用於支持XA事務或JTA事務的鏈接。是BasicManagedDataSource中實際調用的數據源,能夠說BasicManagedDataSource只是封裝了ManagedDataSource

另外,爲了支持JNDIDBCP也提供了相應的類。

類名 描述
InstanceKeyDataSource 用於支持JDNI環境的數據源
PerUserPoolDataSource InstanceKeyDataSource的子類,針對每一個用戶會單獨分配一個鏈接池,每一個鏈接池能夠設置不一樣屬性。例如如下需求,相比user,admin能夠建立更多地鏈接以保證
SharedPoolDataSource InstanceKeyDataSource的子類,不一樣用戶共享一個鏈接池

本文的源碼分析僅會涉及到BasicDataSource(包含它封裝的PoolingDataSource),其餘的數據源暫時不擴展。

BasicDataSource.getConnection()

BasicDataSourceFactory只是簡單地new了一個BasicDataSource對象並初始化配置參數,此時真正的數據源(PoolingDataSource)以及鏈接池(GenericObjectPool<PoolableConnection>)並無建立,而建立的時機爲咱們第一次調用getConnection()的時候。所以,本文直接從BasicDataSourcegetConnection()方法開始分析。

public Connection getConnection() throws SQLException {
        return createDataSource().getConnection();
    }

BasicDataSource.createDataSource()

這個方法會建立數據源和鏈接池,整個過程能夠歸納爲如下幾步:

  1. 註冊MBean,用於支持JMX
  2. 建立鏈接池對象GenericObjectPool<PoolableConnection>
  3. 建立數據源對象PoolingDataSource<PoolableConnection>
  4. 初始化鏈接數;
  5. 開啓空閒資源回收線程(若是設置timeBetweenEvictionRunsMillis爲正數)。
protected DataSource createDataSource() throws SQLException {
        if(closed) {
            throw new SQLException("Data source is closed");
        }
        if(dataSource != null) {
            return dataSource;
        }

        synchronized(this) {
            if(dataSource != null) {
                return dataSource;
            }
            // 註冊MBean,用於支持JMX,這方面的內容不在這裏擴展
            jmxRegister();

            // 建立原生Connection工廠:本質就是持有數據庫驅動對象和幾個鏈接參數
            final ConnectionFactory driverConnectionFactory = createConnectionFactory();

            // 將driverConnectionFactory包裝成池化Connection工廠
            PoolableConnectionFactory poolableConnectionFactory = createPoolableConnectionFactory(driverConnectionFactory);
            // 設置PreparedStatements緩存(其實在這裏能夠發現,上面建立池化工廠時就設置了緩存,這裏不必再設置一遍)
            poolableConnectionFactory.setPoolStatements(poolPreparedStatements);
            poolableConnectionFactory.setMaxOpenPreparedStatements(maxOpenPreparedStatements);

            // 建立數據庫鏈接池對象GenericObjectPool,用於管理鏈接
            // BasicDataSource將持有GenericObjectPool對象
            createConnectionPool(poolableConnectionFactory);

            // 建立PoolingDataSource對象
            // 該對象持有GenericObjectPool對象的引用
            DataSource newDataSource = createDataSourceInstance();
            newDataSource.setLogWriter(logWriter);

            // 根據咱們設置的initialSize建立初始鏈接
            for(int i = 0; i < initialSize; i++) {
                connectionPool.addObject();
            }

            // 開啓鏈接池的evictor線程
            startPoolMaintenance();
            // 最後BasicDataSource將持有上面建立的PoolingDataSource對象
            dataSource = newDataSource;
            return dataSource;
        }
    }

以上方法涉及到幾個類,這裏再補充下UML圖。

GenericObjectPool的UML圖

類名 描述
DriverConnectionFactory 用於生成原生的Connection對象
PoolableConnectionFactory 用於生成池化的Connection對象,持有ConnectionFactory對象的引用
GenericObjectPool 數據庫鏈接池,用於管理鏈接。持有PoolableConnectionFactory對象的引用

獲取鏈接對象

上面已經大體分析了數據源和鏈接池對象的獲取過程,接下來研究下鏈接對象的獲取。在此以前先了解下DBCP中幾個Connection實現類。

DelegatingConnection的UML圖

類名 描述
DelegatingConnection Connection實現類,是如下幾個類的父類
PoolingConnection 用於包裝原生的Connection,支持緩存prepareStatementprepareCall
PoolableConnection 用於包裝原生的PoolingConnection(若是沒有開啓poolPreparedStatements,則包裝的只是原生Connection),調用close()時只是將鏈接還給鏈接池
PoolableManagedConnection PoolableConnection的子類,用於包裝ManagedConnection,支持JTAXA事務
ManagedConnection 用於包裝原生的Connection,支持JTAXA事務
PoolGuardConnectionWrapper 用於包裝PoolableConnection,當accessToUnderlyingConnectionAllowed才能獲取底層鏈接對象。咱們獲取到的就是這個對象

另外,這裏先歸納下得到鏈接的整個過程:

  1. 若是設置了removeAbandonedOnBorrow,達到條件會進行檢測;
  2. 從鏈接池中獲取鏈接,若是沒有就經過工廠建立(經過DriverConnectionFactory建立原生對象,再經過PoolableConnectionFactory包裝爲池化對象);
  3. 經過工廠從新初始化鏈接對象;
  4. 若是設置了testOnBorrow或者testOnCreate,會經過工廠校驗鏈接有效性;
  5. 使用PoolGuardConnectionWrapper包裝鏈接對象,並返回給客戶端

PoolingDataSource.getConnection()

前面已經說過,BasicDataSource本質上是調用PoolingDataSource的方法來獲取鏈接,因此這裏從PoolingDataSource.getConnection()開始研究。

如下代碼可知,該方法會從鏈接池中「借出」鏈接。

public Connection getConnection() throws SQLException {
        // 這個泛型C指的是PoolableConnection對象
        // 調用的是GenericObjectPool的方法返回PoolableConnection對象,這個方法後面會展開
        final C conn = pool.borrowObject();
        if (conn == null) {
            return null;
        }
        // 包裝PoolableConnection對象,當accessToUnderlyingConnectionAllowed爲true時,可使用底層鏈接
        return new PoolGuardConnectionWrapper<>(conn);
    }

GenericObjectPool.borrowObject()

GenericObjectPool是一個很簡練的類,裏面涉及到的屬性設置和鎖機制都涉及得很是巧妙。

// 存放着鏈接池全部的鏈接對象(但不包含已經釋放的)
    private final Map<IdentityWrapper<T>, PooledObject<T>> allObjects =
        new ConcurrentHashMap<>();
    // 存放着空閒鏈接對象的阻塞隊列
    private final LinkedBlockingDeque<PooledObject<T>> idleObjects;
    // 爲n>1表示當前有n個線程正在建立新鏈接對象
    private long makeObjectCount = 0;
    // 建立鏈接對象時所用的鎖
    private final Object makeObjectCountLock = new Object();
    // 鏈接對象建立總數量
    private final AtomicLong createCount = new AtomicLong(0);

    public T borrowObject() throws Exception {
        // 若是咱們設置了鏈接獲取等待時間,「借出」過程就必須在指定時間內完成
        return borrowObject(getMaxWaitMillis());
    }

    public T borrowObject(final long borrowMaxWaitMillis) throws Exception {
        // 校驗鏈接池是否打開狀態
        assertOpen();
        
        // 若是設置了removeAbandonedOnBorrow,達到觸發條件是會遍歷全部鏈接,未使用時長超過removeAbandonedTimeout的將被釋放掉(通常能夠檢測出泄露鏈接)
        final AbandonedConfig ac = this.abandonedConfig;
        if (ac != null && ac.getRemoveAbandonedOnBorrow() &&
                (getNumIdle() < 2) &&
                (getNumActive() > getMaxTotal() - 3) ) {
            removeAbandoned(ac);
        }
        
        
        PooledObject<T> p = null;
        // 鏈接數達到maxTotal是否阻塞等待
        final boolean blockWhenExhausted = getBlockWhenExhausted();

        boolean create;
        final long waitTime = System.currentTimeMillis();
        
        // 若是獲取的鏈接對象爲空,會再次進入獲取
        while (p == null) {
            create = false;
            // 獲取空閒隊列的第一個元素,若是爲空就試圖建立新鏈接
            p = idleObjects.pollFirst();
            if (p == null) {
                // 後面分析這個方法
                p = create();
                if (p != null) {
                    create = true;
                }
            }
            // 鏈接數達到maxTotal且暫時沒有空閒鏈接,這時須要阻塞等待,直到得到空閒隊列中的鏈接或等待超時
            if (blockWhenExhausted) {
                if (p == null) {
                    if (borrowMaxWaitMillis < 0) {
                        // 無限等待
                        p = idleObjects.takeFirst();
                    } else {
                        // 等待maxWaitMillis
                        p = idleObjects.pollFirst(borrowMaxWaitMillis,
                                TimeUnit.MILLISECONDS);
                    }
                }
                // 這個時候仍是沒有就只能拋出異常
                if (p == null) {
                    throw new NoSuchElementException(
                            "Timeout waiting for idle object");
                }
            } else {
                if (p == null) {
                    throw new NoSuchElementException("Pool exhausted");
                }
            }
            // 若是鏈接處於空閒狀態,會修改鏈接的state、lastBorrowTime、lastUseTime、borrowedCount等,並返回true
            if (!p.allocate()) {
                p = null;
            }

            if (p != null) {
                // 利用工廠從新初始化鏈接對象,這裏會去校驗鏈接存活時間、設置lastUsedTime、及其餘初始參數
                try {
                    factory.activateObject(p);
                } catch (final Exception e) {
                    try {
                        destroy(p);
                    } catch (final Exception e1) {
                        // Ignore - activation failure is more important
                    }
                    p = null;
                    if (create) {
                        final NoSuchElementException nsee = new NoSuchElementException(
                                "Unable to activate object");
                        nsee.initCause(e);
                        throw nsee;
                    }
                }
                // 根據設置的參數,判斷是否檢測鏈接有效性
                if (p != null && (getTestOnBorrow() || create && getTestOnCreate())) {
                    boolean validate = false;
                    Throwable validationThrowable = null;
                    try {
                        // 這裏會去校驗鏈接的存活時間是否超過maxConnLifetimeMillis,以及經過SQL去校驗執行時間
                        validate = factory.validateObject(p);
                    } catch (final Throwable t) {
                        PoolUtils.checkRethrow(t);
                        validationThrowable = t;
                    }
                    // 若是校驗不經過,會釋放該對象
                    if (!validate) {
                        try {
                            destroy(p);
                            destroyedByBorrowValidationCount.incrementAndGet();
                        } catch (final Exception e) {
                            // Ignore - validation failure is more important
                        }
                        p = null;
                        if (create) {
                            final NoSuchElementException nsee = new NoSuchElementException(
                                    "Unable to validate object");
                            nsee.initCause(validationThrowable);
                            throw nsee;
                        }
                    }
                }
            }
        }
        // 更新borrowedCount、idleTimes和waitTimes
        updateStatsBorrow(p, System.currentTimeMillis() - waitTime);

        return p.getObject();
    }

GenericObjectPool.create()

這裏在建立鏈接對象時採用的鎖機制很是值得學習,簡練且高效。

private PooledObject<T> create() throws Exception {
        int localMaxTotal = getMaxTotal();
        if (localMaxTotal < 0) {
            localMaxTotal = Integer.MAX_VALUE;
        }

        final long localStartTimeMillis = System.currentTimeMillis();
        final long localMaxWaitTimeMillis = Math.max(getMaxWaitMillis(), 0);

        // 建立標識:
        // - TRUE:  調用工廠建立返回對象
        // - FALSE: 直接返回null
        // - null:  繼續循環
        Boolean create = null;
        while (create == null) {
            synchronized (makeObjectCountLock) {
                final long newCreateCount = createCount.incrementAndGet();
                if (newCreateCount > localMaxTotal) {
                    // 當前池已經達到maxTotal,或者有另一個線程正在試圖建立一個新的鏈接使之達到容量極限
                    createCount.decrementAndGet();
                    if (makeObjectCount == 0) {
                        // 鏈接池確實已達到容量極限
                        create = Boolean.FALSE;
                    } else {
                        // 當前另一個線程正在試圖建立一個新的鏈接使之達到容量極限,此時須要等待
                        makeObjectCountLock.wait(localMaxWaitTimeMillis);
                    }
                } else {
                    // 當前鏈接池容量未到達極限,能夠繼續建立鏈接對象
                    makeObjectCount++;
                    create = Boolean.TRUE;
                }
            }

            // 當達到maxWaitTimeMillis時不建立鏈接對象,直接退出循環
            if (create == null &&
                (localMaxWaitTimeMillis > 0 &&
                 System.currentTimeMillis() - localStartTimeMillis >= localMaxWaitTimeMillis)) {
                create = Boolean.FALSE;
            }
        }

        if (!create.booleanValue()) {
            return null;
        }

        final PooledObject<T> p;
        try {
            // 調用工廠建立對象,後面對這個方法展開分析
            p = factory.makeObject();
        } catch (final Throwable e) {
            createCount.decrementAndGet();
            throw e;
        } finally {
            synchronized (makeObjectCountLock) {
                // 建立標識-1
                makeObjectCount--;
                // 喚醒makeObjectCountLock鎖住的對象
                makeObjectCountLock.notifyAll();
            }
        }

        final AbandonedConfig ac = this.abandonedConfig;
        if (ac != null && ac.getLogAbandoned()) {
            p.setLogAbandoned(true);
            // TODO: in 3.0, this can use the method defined on PooledObject
            if (p instanceof DefaultPooledObject<?>) {
                ((DefaultPooledObject<T>) p).setRequireFullStackTrace(ac.getRequireFullStackTrace());
            }
        }
        // 鏈接數量+1
        createdCount.incrementAndGet();
        // 將建立的對象放入allObjects
        allObjects.put(new IdentityWrapper<>(p.getObject()), p);
        return p;
    }

PoolableConnectionFactory.makeObject()

public PooledObject<PoolableConnection> makeObject() throws Exception {
        // 建立原生的Connection對象
        Connection conn = connectionFactory.createConnection();
        if (conn == null) {
            throw new IllegalStateException("Connection factory returned null from createConnection");
        }
        try {
            // 執行咱們設置的connectionInitSqls
            initializeConnection(conn);
        } catch (final SQLException sqle) {
            // Make sure the connection is closed
            try {
                conn.close();
            } catch (final SQLException ignore) {
                // ignore
            }
            // Rethrow original exception so it is visible to caller
            throw sqle;
        }
        // 鏈接索引+1
        final long connIndex = connectionIndex.getAndIncrement();
        
        // 若是設置了poolPreparedStatements,則建立包裝鏈接爲PoolingConnection對象
        if (poolStatements) {
            conn = new PoolingConnection(conn);
            final GenericKeyedObjectPoolConfig<DelegatingPreparedStatement> config = new GenericKeyedObjectPoolConfig<>();
            config.setMaxTotalPerKey(-1);
            config.setBlockWhenExhausted(false);
            config.setMaxWaitMillis(0);
            config.setMaxIdlePerKey(1);
            config.setMaxTotal(maxOpenPreparedStatements);
            if (dataSourceJmxObjectName != null) {
                final StringBuilder base = new StringBuilder(dataSourceJmxObjectName.toString());
                base.append(Constants.JMX_CONNECTION_BASE_EXT);
                base.append(Long.toString(connIndex));
                config.setJmxNameBase(base.toString());
                config.setJmxNamePrefix(Constants.JMX_STATEMENT_POOL_PREFIX);
            } else {
                config.setJmxEnabled(false);
            }
            final PoolingConnection poolingConn = (PoolingConnection) conn;
            final KeyedObjectPool<PStmtKey, DelegatingPreparedStatement> stmtPool = new GenericKeyedObjectPool<>(
                    poolingConn, config);
            poolingConn.setStatementPool(stmtPool);
            poolingConn.setCacheState(cacheState);
        }

        // 用於註冊鏈接到JMX
        ObjectName connJmxName;
        if (dataSourceJmxObjectName == null) {
            connJmxName = null;
        } else {
            connJmxName = new ObjectName(
                    dataSourceJmxObjectName.toString() + Constants.JMX_CONNECTION_BASE_EXT + connIndex);
        }
        
        // 建立PoolableConnection對象
        final PoolableConnection pc = new PoolableConnection(conn, pool, connJmxName, disconnectionSqlCodes,
                fastFailValidation);
        pc.setCacheState(cacheState);
        
        // 包裝成鏈接池所需的對象
        return new DefaultPooledObject<>(pc);
    }

空閒對象回收器Evictor

以上基本已分析完鏈接對象的獲取過程,下面再研究下空閒對象回收器。前面已經講到當建立完數據源對象時會開啓鏈接池的evictor線程,因此咱們從BasicDataSource.startPoolMaintenance()開始分析。

BasicDataSource.startPoolMaintenance()

前面說過timeBetweenEvictionRunsMillis爲非正數時不會開啓開啓空閒對象回收器,從如下代碼能夠理解具體邏輯。

protected void startPoolMaintenance() {
        // 只有timeBetweenEvictionRunsMillis爲正數,纔會開啓空閒對象回收器
        if (connectionPool != null && timeBetweenEvictionRunsMillis > 0) {
            connectionPool.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
        }
    }

BaseGenericObjectPool.setTimeBetweenEvictionRunsMillis(long)

這個BaseGenericObjectPool是上面說到的GenericObjectPool的父類。

public final void setTimeBetweenEvictionRunsMillis(
            final long timeBetweenEvictionRunsMillis) {
        // 設置回收線程運行間隔時間
        this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
        // 繼續調用本類的方法,下面繼續進入方法分析
        startEvictor(timeBetweenEvictionRunsMillis);
    }

BaseGenericObjectPool.startEvictor(long)

這裏會去定義一個Evictor對象,這個實際上是一個Runnable對象,後面會講到。

final void startEvictor(final long delay) {
        synchronized (evictionLock) {
            if (null != evictor) {
                EvictionTimer.cancel(evictor, evictorShutdownTimeoutMillis, TimeUnit.MILLISECONDS);
                evictor = null;
                evictionIterator = null;
            }
            // 建立回收器任務,並執行定時調度
            if (delay > 0) {
                evictor = new Evictor();
                EvictionTimer.schedule(evictor, delay, delay);
            }
        }
    }

EvictionTimer.schedule(Evictor, long, long)

DBCP是使用ScheduledThreadPoolExecutor來實現回收器的定時檢測。 涉及到ThreadPoolExecutorJDK自帶的api,這裏再也不深刻分析線程池如何實現定時調度。感興趣的朋友能夠複習下經常使用的幾款線程池。

static synchronized void schedule(
            final BaseGenericObjectPool<?>.Evictor task, final long delay, final long period) 
        if (null == executor) {
            // 建立線程池,隊列爲DelayedWorkQueue,corePoolSize爲1,maximumPoolSize爲無限大
            executor = new ScheduledThreadPoolExecutor(1, new EvictorThreadFactory());
            // 當任務被取消的同時從等待隊列中移除
            executor.setRemoveOnCancelPolicy(true);
        }
        // 設置任務定時調度
        final ScheduledFuture<?> scheduledFuture =
                executor.scheduleWithFixedDelay(task, delay, period, TimeUnit.MILLISECONDS);
        task.setScheduledFuture(scheduledFuture);
    }

BaseGenericObjectPool.Evictor

EvictorBaseGenericObjectPool的內部類,實現了Runnable接口,這裏看下它的run方法。

class Evictor implements Runnable {

        private ScheduledFuture<?> scheduledFuture;

        @Override
        public void run() {
            final ClassLoader savedClassLoader =
                    Thread.currentThread().getContextClassLoader();
            try {
                // 確保回收器使用的類加載器和工廠對象的同樣
                if (factoryClassLoader != null) {
                    final ClassLoader cl = factoryClassLoader.get();
                    if (cl == null) {
                        cancel();
                        return;
                    }
                    Thread.currentThread().setContextClassLoader(cl);
                }

               
                try {
                // 回收符合條件的對象,後面繼續擴展
                    evict();
                } catch(final Exception e) {
                    swallowException(e);
                } catch(final OutOfMemoryError oome) {
                    // Log problem but give evictor thread a chance to continue
                    // in case error is recoverable
                    oome.printStackTrace(System.err);
                }
                try {
                    // 確保最小空閒對象
                    ensureMinIdle();
                } catch (final Exception e) {
                    swallowException(e);
                }
            } finally {
                Thread.currentThread().setContextClassLoader(savedClassLoader);
            }
        }


        void setScheduledFuture(final ScheduledFuture<?> scheduledFuture) {
            this.scheduledFuture = scheduledFuture;
        }


        void cancel() {
            scheduledFuture.cancel(false);
        }
    }

GenericObjectPool.evict()

這裏的回收過程包括如下四道校驗:

  1. 按照evictionPolicy校驗idleSoftEvictTimeidleEvictTime

  2. 利用工廠從新初始化樣本,這裏會校驗maxConnLifetimeMillistestWhileIdle爲true);

  3. 校驗maxConnLifetimeMillisvalidationQueryTimeouttestWhileIdle爲true);

  4. 校驗全部鏈接的未使用時間是否超過removeAbandonedTimeoutremoveAbandonedOnMaintenance爲true)。

public void evict() throws Exception {
        // 校驗當前鏈接池是否關閉
        assertOpen();

        if (idleObjects.size() > 0) {

            PooledObject<T> underTest = null;
            // 介紹參數時已經講到,這個evictionPolicy咱們能夠自定義
            final EvictionPolicy<T> evictionPolicy = getEvictionPolicy();

            synchronized (evictionLock) {
                final EvictionConfig evictionConfig = new EvictionConfig(
                        getMinEvictableIdleTimeMillis(),
                        getSoftMinEvictableIdleTimeMillis(),
                        getMinIdle());

                final boolean testWhileIdle = getTestWhileIdle();
                // 獲取咱們指定的樣本數,並開始遍歷
                for (int i = 0, m = getNumTests(); i < m; i++) {
                    if (evictionIterator == null || !evictionIterator.hasNext()) {
                        evictionIterator = new EvictionIterator(idleObjects);
                    }
                    if (!evictionIterator.hasNext()) {
                        // Pool exhausted, nothing to do here
                        return;
                    }

                    try {
                        underTest = evictionIterator.next();
                    } catch (final NoSuchElementException nsee) {
                        // 當前樣本正被另外一個線程借出
                        i--;
                        evictionIterator = null;
                        continue;
                    }
                    // 判斷若是樣本是空閒狀態,設置爲EVICTION狀態
                    // 若是不是,說明另外一個線程已經借出了這個樣本
                    if (!underTest.startEvictionTest()) {
                        i--;
                        continue;
                    }

                    boolean evict;
                    try {
                        // 調用回收策略來判斷是否回收該樣本,按照默認策略,如下狀況都會返回true:
                        // 1. 樣本空閒時間大於咱們設置的idleSoftEvictTime,且當前池中空閒鏈接數量>minIdle
                        // 2.  樣本空閒時間大於咱們設置的idleEvictTime
                        evict = evictionPolicy.evict(evictionConfig, underTest,
                                idleObjects.size());
                    } catch (final Throwable t) {
                        PoolUtils.checkRethrow(t);
                        swallowException(new Exception(t));
                        evict = false;
                    }
                    // 若是須要回收,則釋放這個樣本
                    if (evict) {
                        destroy(underTest);
                        destroyedByEvictorCount.incrementAndGet();
                    } else {
                        // 若是設置了testWhileIdle,會
                        if (testWhileIdle) {
                            boolean active = false;
                            try {
                                // 利用工廠從新初始化樣本,這裏會校驗maxConnLifetimeMillis
                                factory.activateObject(underTest);
                                active = true;
                            } catch (final Exception e) {
                                // 拋出異常標識校驗不經過,釋放樣本
                                destroy(underTest);
                                destroyedByEvictorCount.incrementAndGet();
                            }
                            if (active) {
                                // 接下來會校驗maxConnLifetimeMillis和validationQueryTimeout
                                if (!factory.validateObject(underTest)) {
                                    destroy(underTest);
                                    destroyedByEvictorCount.incrementAndGet();
                                } else {
                                    try {
                                        // 這裏會將樣本rollbackOnReturn、autoCommitOnReturn等
                                        factory.passivateObject(underTest);
                                    } catch (final Exception e) {
                                        destroy(underTest);
                                        destroyedByEvictorCount.incrementAndGet();
                                    }
                                }
                            }
                        }
                        // 若是狀態爲EVICTION或EVICTION_RETURN_TO_HEAD,修改成IDLE
                        if (!underTest.endEvictionTest(idleObjects)) {
                            //空
                        }
                    }
                }
            }
        }
        // 校驗全部鏈接的未使用時間是否超過removeAbandonedTimeout
        final AbandonedConfig ac = this.abandonedConfig;
        if (ac != null && ac.getRemoveAbandonedOnMaintenance()) {
            removeAbandoned(ac);
        }
    }

以上已基本研究完數據源建立、鏈接對象獲取和空閒資源回收器,後續有空再作補充。

經過JNDI獲取數據源對象

需求

本文測試使用JNDI獲取PerUserPoolDataSourceSharedPoolDataSource對象,選擇使用tomcat 9.0.21做容器。

若是以前沒有接觸過JNDI,並不會影響下面例子的理解,其實能夠理解爲像springbean配置和獲取。

源碼分析時已經講到,除了咱們熟知的BasicDataSourceDBCP還提供了經過JDNI獲取數據源,以下表。

類名 描述
InstanceKeyDataSource 用於支持JDNI環境的數據源,是如下兩個類的父類
PerUserPoolDataSource InstanceKeyDataSource的子類,針對每一個用戶會單獨分配一個鏈接池,每一個鏈接池能夠設置不一樣屬性。例如如下需求,相比user,admin能夠建立更多地鏈接以保證
SharedPoolDataSource InstanceKeyDataSource的子類,不一樣用戶共享一個鏈接池

引入依賴

本文在前面例子的基礎上增長如下依賴,由於是web項目,因此打包方式爲war

<dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>javax.servlet.jsp-api</artifactId>
            <version>2.2.1</version>
            <scope>provided</scope>
        </dependency>

編寫context.xml

webapp文件下建立目錄META-INF,並建立context.xml文件。這裏面的每一個resource節點都是咱們配置的對象,相似於springbean節點。其中bean/DriverAdapterCPDS這個對象須要被另外兩個使用到。

<?xml version="1.0" encoding="UTF-8"?>
<Context>
    <Resource 
        name="bean/SharedPoolDataSourceFactory" 
        auth="Container"
        type="org.apache.commons.dbcp2.datasources.SharedPoolDataSource"
        factory="org.apache.commons.dbcp2.datasources.SharedPoolDataSourceFactory" 
        singleton="false" 
        driverClassName="com.mysql.cj.jdbc.Driver"
        url="jdbc:mysql://localhost:3306/github_demo?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=GMT%2B8&amp;useSSL=true" 
        username="root"
        password="root" 
        maxTotal="8" 
        maxIdle="10" 
        dataSourceName="java:comp/env/bean/DriverAdapterCPDS"
         />
    <Resource 
        name="bean/PerUserPoolDataSourceFactory" 
        auth="Container"
        type="org.apache.commons.dbcp2.datasources.PerUserPoolDataSource"
        factory="org.apache.commons.dbcp2.datasources.PerUserPoolDataSourceFactory" 
        singleton="false" 
        driverClassName="com.mysql.cj.jdbc.Driver"
        url="jdbc:mysql://localhost:3306/github_demo?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=GMT%2B8&amp;useSSL=true" 
        username="root"
        password="root" 
        maxTotal="8" 
        maxIdle="10" 
        dataSourceName="java:comp/env/bean/DriverAdapterCPDS"
         />      
    <Resource 
        name="bean/DriverAdapterCPDS" 
        auth="Container"
        type="org.apache.commons.dbcp2.cpdsadapter.DriverAdapterCPDS"
        factory="org.apache.commons.dbcp2.cpdsadapter.DriverAdapterCPDS" 
        singleton="false" 
        driverClassName="com.mysql.cj.jdbc.Driver"
        url="jdbc:mysql://localhost:3306/github_demo?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=GMT%2B8&amp;useSSL=true" 
        userName="root"
        userPassword="root" 
        maxIdle="10" 
         />      
</Context>

編寫web.xml

web-app節點下配置資源引用,每一個resource-env-ref指向了咱們配置好的對象。

<resource-env-ref>
        <description>Test DriverAdapterCPDS</description>
        <resource-env-ref-name>bean/DriverAdapterCPDS</resource-env-ref-name>
        <resource-env-ref-type>org.apache.commons.dbcp2.cpdsadapter.DriverAdapterCPDS</resource-env-ref-type>        
    </resource-env-ref> 
    <resource-env-ref>
        <description>Test SharedPoolDataSource</description>
        <resource-env-ref-name>bean/SharedPoolDataSourceFactory</resource-env-ref-name>
        <resource-env-ref-type>org.apache.commons.dbcp2.datasources.SharedPoolDataSource</resource-env-ref-type>        
    </resource-env-ref>
    <resource-env-ref>
        <description>Test erUserPoolDataSource</description>
        <resource-env-ref-name>bean/erUserPoolDataSourceFactory</resource-env-ref-name>
        <resource-env-ref-type>org.apache.commons.dbcp2.datasources.erUserPoolDataSource</resource-env-ref-type>        
    </resource-env-ref>

編寫jsp

由於須要在web環境中使用,若是直接建類寫個main方法測試,會一直報錯的,目前沒找到好的辦法。這裏就簡單地使用jsp來測試吧(這是從tomcat官網參照的例子)。

<body>
    <%  
        // 得到名稱服務的上下文對象
        Context initCtx = new InitialContext();
        Context envCtx = (Context)initCtx.lookup("java:comp/env/");
        
        // 查找指定名字的對象
        DataSource ds = (DataSource)envCtx.lookup("bean/SharedPoolDataSourceFactory");
        
        DataSource ds2 = (DataSource)envCtx.lookup("bean/PerUserPoolDataSourceFactory");        
        // 獲取鏈接
        Connection conn = ds.getConnection("root","root");
        System.out.println("conn" + conn); 
        Connection conn2 = ds2.getConnection("zzf","zzf");
        System.out.println("conn2" + conn2); 
        
        // ... 使用鏈接操做數據庫,以及釋放資源 ...
        conn.close();
        conn2.close();
    %>
</body>

測試結果

打包項目在tomcat9上運行,訪問 http://localhost:8080/DBCP-demo/testInstanceKeyDataSource.jsp ,控制檯打印以下內容:

conn=1971654708, URL=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true, UserName=root@localhost, MySQL Connector/J
conn2=128868782, URL=jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true, UserName=zzf@localhost, MySQL Connector/J

使用DBCP測試兩階段提交

前面源碼分析已經講到,如下類用於支持JTA事務。本文將介紹如何使用DBCP來實現JTA事務兩階段提交(固然,實際項目並不支持使用2PC,由於性能開銷太大)。

類名 描述
BasicManagedDataSource BasicDataSource的子類,用於建立支持XA事務或JTA事務的鏈接
ManagedDataSource PoolingDataSource的子類,用於支持XA事務或JTA事務的鏈接。是BasicManagedDataSource中實際調用的數據源,能夠說BasicManagedDataSource只是封裝了ManagedDataSource

準備工做

由於測試例子使用的是mysql,使用XA事務須要開啓支持。注意,mysql只有innoDB引擎才支持(另外,XA事務和常規事務是互斥的,若是開啓了XA事務,其餘線程進來即便只讀也是不行的)。

SHOW VARIABLES LIKE '%xa%' -- 查看XA事務是否開啓
SET innodb_support_xa = ON -- 開啓XA事務

除了原來的github_demo數據庫,我另外建了一個test數據庫,簡單地模擬兩個數據庫。

mysql的XA事務使用

測試以前,這裏簡單回顧下直接使用sql操做XA事務的過程,將有助於對如下內容的理解:

XA START 'my_test_xa'; -- 啓動一個xid爲my_test_xa的事務,並使之爲active狀態
UPDATE github_demo.demo_user SET deleted = 1 WHERE id = '1'; -- 事務中的語句
XA END 'my_test_xa'; -- 把事務置爲idle狀態
XA PREPARE 'my_test_xa'; -- 把事務置爲prepare狀態
XA COMMIT 'my_test_xa'; -- 提交事務
XA ROLLBACK 'my_test_xa'; -- 回滾事務
XA RECOVER; -- 查看處於prepare狀態的事務列表

引入依賴

在入門例子的基礎上,增長如下依賴,本文采用第三方atomikos的實現。

<!-- jta:用於測試DBCP對JTA事務的支持 -->
        <dependency>
            <groupId>javax.transaction</groupId>
            <artifactId>jta</artifactId>
            <version>1.1</version>
        </dependency>
        <dependency>
            <groupId>com.atomikos</groupId>
            <artifactId>transactions-jdbc</artifactId>
            <version>3.9.3</version>
        </dependency>

獲取BasicManagedDataSource

這裏千萬記得要設置DefaultCatalog,不然當前事務中註冊不一樣資源管理器時,可能都會被當成同一個資源管理器而拒絕註冊並報錯,由於這個問題,花了我好長時間才解決。

public BasicManagedDataSource getBasicManagedDataSource(
            TransactionManager transactionManager, 
            String url, 
            String username, 
            String password) {
        BasicManagedDataSource basicManagedDataSource = new BasicManagedDataSource();
        basicManagedDataSource.setTransactionManager(transactionManager);
        basicManagedDataSource.setUrl(url);
        basicManagedDataSource.setUsername(username);
        basicManagedDataSource.setPassword(password);
        basicManagedDataSource.setDefaultAutoCommit(false);
        basicManagedDataSource.setXADataSource("com.mysql.cj.jdbc.MysqlXADataSource");
        return basicManagedDataSource;
    }
    @Test
    public void test01() throws Exception {
        // 得到事務管理器
        TransactionManager transactionManager = new UserTransactionManager();
        
        // 獲取第一個數據庫的數據源
        BasicManagedDataSource basicManagedDataSource1 = getBasicManagedDataSource(
                transactionManager, 
                "jdbc:mysql://localhost:3306/github_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true", 
                "root", 
                "root");
        // 注意,這一步很是重要
        basicManagedDataSource1.setDefaultCatalog("github_demo");
        
        // 獲取第二個數據庫的數據源
        BasicManagedDataSource basicManagedDataSource2 = getBasicManagedDataSource(
                transactionManager, 
                "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=true", 
                "zzf", 
                "zzf");
        // 注意,這一步很是重要
        basicManagedDataSource1.setDefaultCatalog("test");
    }

編寫兩階段提交的代碼

經過運行代碼能夠發現,當數據庫1和2的操做都成功,纔會提交,只要其中一個數據庫執行失敗,兩個操做都會回滾。

@Test
    public void test01() throws Exception { 
        Connection connection1 = null;
        Statement statement1 = null;
        Connection connection2 = null;
        Statement statement2 = null;
        transactionManager.begin();
        try {
            // 獲取鏈接並進行數據庫操做,這裏會將會將XAResource註冊到當前線程的XA事務對象
            /**
             * XA START xid1;-- 啓動一個事務,並使之爲active狀態
             */
            connection1 = basicManagedDataSource1.getConnection();
            statement1 = connection1.createStatement();
            /**
             * update github_demo.demo_user set deleted = 1 where id = '1'; -- 事務中的語句
             */
            boolean result1 = statement1.execute("update github_demo.demo_user set deleted = 1 where id = '1'");
            System.out.println(result1);
            
            /**
             * XA START xid2;-- 啓動一個事務,並使之爲active狀態
             */
            connection2 = basicManagedDataSource2.getConnection();
            statement2 = connection2.createStatement();
            /**
             * update test.demo_user set deleted = 1 where id = '1'; -- 事務中的語句
             */
            boolean result2 = statement2.execute("update test.demo_user set deleted = 1 where id = '1'");
            System.out.println(result2);
            
            /**
             * 當這執行如下語句:
             * XA END xid1; -- 把事務置爲idle狀態
             * XA PREPARE xid1; -- 把事務置爲prepare狀態
             * XA END xid2; -- 把事務置爲idle狀態
             * XA PREPARE xid2; -- 把事務置爲prepare狀態   
             * XA COMMIT xid1; -- 提交事務
             * XA COMMIT xid2; -- 提交事務
             */
            transactionManager.commit();
        } catch(Exception e) {
            e.printStackTrace();
        } finally {
            statement1.close();
            statement2.close();
            connection1.close();
            connection2.close();
        }
    }

相關源碼請移步:https://github.com/ZhangZiSheng001/dbcp-demo

本文爲原創文章,轉載請附上原文出處連接:https://www.cnblogs.com/ZhangZiSheng001/p/12003922.html

相關文章
相關標籤/搜索