DBCP2的使用例子和原始碼詳解(包括JNDI和JTA支援的使用)
目錄
- 簡介
- 使用例子
- 需求
- 工程環境
- 主要步驟
- 建立專案
- 引入依賴
- 編寫
jdbc.prperties
- 獲取連線池和獲取連線
- 編寫測試類
- 配置檔案詳解
- 資料庫連線引數
- 連線池資料基本引數
- 連線檢查引數
- 快取語句
- 事務相關引數
- 連線洩漏回收引數
- 其他
- 原始碼分析
- 資料來源建立
BasicDataSource.getConnection()
BasicDataSource.createDataSource()
- 獲取連線物件
PoolingDataSource.getConnection()
GenericObjectPool.borrowObject()
GenericObjectPool.create()
PoolableConnectionFactory.makeObject()
- 空閒物件回收器
Evictor
BasicDataSource.startPoolMaintenance()
BaseGenericObjectPool.setTimeBetweenEvictionRunsMillis(long)
BaseGenericObjectPool.startEvictor(long)
EvictionTimer.schedule(Evictor, long, long)
BaseGenericObjectPool.Evictor
GenericObjectPool.evict()
- 資料來源建立
- 通過
JNDI
- 需求
- 引入依賴
- 編寫
context.xml
- 編寫
web.xml
- 編寫
jsp
- 測試結果
- 使用
DBCP
測試兩階段提交- 準備工作
mysql
的XA
事務使用- 引入依賴
- 獲取
BasicManagedDataSource
- 編寫兩階段提交的程式碼
簡介
DBCP
用於建立和管理連線,利用“池”的方式複用連線減少資源開銷。目前,tomcat
自帶的連線池就是DBCP
,Spring開發組也推薦使用DBCP
。
DBCP
除了我們熟知的使用方式外,還支援通過JNDI
獲取資料來源,並支援獲取JTA
或XA
事務中用於2PC
(兩階段提交)的連線物件,本文也將以例子說明。
本文將包含以下內容(因為篇幅較長,可根據需要選擇閱讀):
DBCP
的使用方法(入門案例說明);DBCP
的配置引數詳解;DBCP
主要原始碼分析;DBCP
其他特性的使用方法,如JNDI
和JTA
支援。
使用例子
需求
使用DBCP
連線池獲取連線物件,對使用者資料進行簡單的增刪改查。
工程環境
JDK
:1.8.0_201
maven
:3.6.1
IDE
:eclipse 4.12
mysql-connector-java
:8.0.15
mysql
:5.7
DBCP
:2.6.0
主要步驟
編寫
jdbc.properties
,設定資料庫連線引數和連線池基本引數等。通過
BasicDataSourceFactory
載入jdbc.properties
,並獲得BasicDataDource
物件。通過
BasicDataDource
物件獲取Connection
物件。使用
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>
編寫jdbc.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
maxActive=8
#最大空閒連線
#連線池中容許保持空閒狀態的最大連線數量,超過的空閒連線將被釋放,如果設定為負數表示不限制
#預設為8
maxIdle=8
#最小空閒連線
#連線池中容許保持空閒狀態的最小連線數量,低於這個數量將建立新的連線,如果設定為0則不建立
#預設為0
minIdle=0
#最大等待時間
#當沒有可用連線時,連線池等待連線被歸還的最大時間(以毫秒計數),超過時間則丟擲異常,如果設定為-1表示無限等待
#預設無限
maxWait=-1
獲取連線池和獲取連線
專案中編寫了JDBCUtil
來初始化連線池、獲取連線、管理事務和釋放資源等,具體參見專案原始碼。
路徑:cn.zzs.dbcp
// 匯入配置檔案
Properties properties = new Properties();
InputStream in = JDBCUtil.class.getClassLoader().getResourceAsStream("jdbc.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 = JDBCUtil.getConnection();
// 開啟事務設定非自動提交
JDBCUtil.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();
// 提交事務
JDBCUtil.commit();
} catch(Exception e) {
JDBCUtil.rollback();
log.error("儲存使用者失敗", e);
} finally {
// 釋放資源
JDBCUtil.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則不建立
#注意:需要開啟空閒物件回收器,這個引數才能生效。
#預設為0
minIdle=0
#最大等待時間
#當沒有可用連線時,連線池等待連線被歸還的最大時間(以毫秒計數),超過時間則丟擲異常,如果設定為<=0表示無限等待
#預設-1
maxWaitMillis=-1
連線檢查引數
針對連線失效和連線洩露的問題,建議開啟testOnBorrow
和空閒資源回收器。
#-------------連線檢查情況--------------------------------
#通過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
快取語句
快取語句建議開啟。
#-------------快取語句--------------------------------
#是否快取PreparedStatements,這個功能在一些支援遊標的資料庫中可以極大提高效能(Oracle、SQL Server、DB2、Sybase)
#預設為false
poolPreparedStatements=false
#快取PreparedStatements的最大個數
#預設為-1
#注意:poolPreparedStatements為true時,這個引數才有效
maxOpenPreparedStatements=-1
#快取read-only和auto-commit狀態。設定為true的話,所有連線的狀態都會是一樣的。
#預設是true
cacheState=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
#連線池建立的連線的預設的資料庫名,如果是使用DBCP的XA連線必須設定,不然註冊不了多個資源管理器
#defaultCatalog=github_demo
#連線池建立的連線的預設的schema。如果是mysql,這個設定沒什麼用。
#defaultSchema=github_demo
連線洩漏回收引數
當我們從連線池獲得了連線物件,但因為疏忽或其他原因沒有close
,這個時候這個連線物件就是一個洩露資源。通過配置以下引數可以回收這部分物件。
#-------------連線洩漏回收引數--------------------------------
#當未使用的時間超過removeAbandonedTimeout時,是否視該連線為洩露連線並刪除(當getConnection()被呼叫時檢測)
#預設為false
#注意:這個機制在(getNumIdle() < 2) and (getNumActive() > (getMaxActive() - 3))時被觸發
removeAbandonedOnBorrow=false
#當未使用的時間超過removeAbandonedTimeout時,是否視該連線為洩露連線並刪除
#預設為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名
#jmxName=
#查詢超時時間
#預設為空,即根據驅動設定
#defaultQueryTimeout=
#控制PoolGuard是否容許獲取底層連線
#預設為false
accessToUnderlyingConnectionAllowed=false
#如果容許則可以使用下面的方式來獲取底層物理連線:
# Connection conn = ds.getConnection();
# Connection dconn = ((DelegatingConnection) conn).getInnermostDelegate();
# ...
# conn.close();
原始碼分析
通過使用例子可知,DBCP
的BasicDataSource
是我們獲取連線物件的入口,至於BasicDataSourceFactory
只是建立和初始化BasicDataSource
例項,它的程式碼就不看了。這裡直接從BasicDataSource
的getConnection()
方法開始分析。
注意:考慮篇幅和可讀性,以下程式碼經過刪減,僅保留所需部分。
資料來源建立
研究資料來源建立之前,先來看下DBCP
的幾種資料來源:
類名 | 描述 |
---|---|
BasicDataSource |
用於滿足基本資料庫操作需求的資料來源 |
BasicManagedDataSource |
BasicDataSource 的子類,用於建立支援XA 事務或JTA 事務的連線 |
PoolingDataSource |
BasicDataSource 中實際呼叫的資料來源,可以說BasicDataSource 只是封裝了PoolingDataSource |
ManagedDataSource |
PoolingDataSource 的子類,用於支援XA 事務或JTA 事務的連線。是BasicManagedDataSource 中實際呼叫的資料來源,可以說BasicManagedDataSource 只是封裝了ManagedDataSource |
InstanceKeyDataSource |
用於支援JDNI 環境的資料來源 |
PerUserPoolDataSource |
InstanceKeyDataSource 的子類,針對每個使用者會單獨分配一個連線池,每個連線池可以設定不同屬性。例如以下需求,相比user,admin 可以建立更多地連線以保證 |
SharedPoolDataSource |
InstanceKeyDataSource 的子類,不同使用者共享一個連線池 |
本文的原始碼分析僅會涉及到BasicDataSource
(包含它封裝的PoolingDataSource
),其他的資料來源暫時不擴充套件。
BasicDataSource.getConnection()
BasicDataSource
是在第一次被呼叫獲取獲取連線時才建立PoolingDataSource
物件。
public Connection getConnection() throws SQLException {
return createDataSource().getConnection();
}
BasicDataSource.createDataSource()
接下來的方法又會涉及到四個類,如下:
類名 | 描述 |
---|---|
ConnectionFactory |
用於生成原生的Connection物件 |
PoolableConnectionFactory |
用於生成包裝過的Connection物件,持有ConnectionFactory 物件的引用 |
GenericObjectPool |
資料庫連線池,用於管理連線。持有PoolableConnectionFactory 物件的引用 |
PoolingDataSource |
資料來源,持有GenericObjectPool 的引用。我們呼叫BasicDataSource 獲取連線物件,實際上呼叫的是它的getConnection() 方法 |
// 資料來源
private volatile DataSource dataSource;
// 連線池
private volatile GenericObjectPool<PoolableConnection> connectionPool;
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工廠
boolean success = false;
PoolableConnectionFactory poolableConnectionFactory;
try {
poolableConnectionFactory = createPoolableConnectionFactory(driverConnectionFactory);
// 設定PreparedStatements快取(其實在這裡可以發現,上面建立池化工廠時就設定了快取,這裡沒必要再設定一遍)
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) {
// 建立資料庫連線池物件GenericObjectPool,用於管理連線
// BasicDataSource將持有GenericObjectPool物件
createConnectionPool(poolableConnectionFactory);
}
// 建立PoolingDataSource物件
//該物件持有GenericObjectPool物件的引用
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();
}
}
// 根據我們設定的initialSize建立初始連線
try {
for (int i = 0; i < initialSize; i++) {
connectionPool.addObject();
}
} catch (final Exception e) {
closeConnectionPool();
throw new SQLException("Error preloading the connection pool", e);
}
// 開啟連線池的evictor執行緒
startPoolMaintenance();
// 最後BasicDataSource將持有上面建立的PoolingDataSource物件
dataSource = newDataSource;
return dataSource;
}
}
獲取連線物件
上面已經大致分析了資料來源物件的獲取過程,接下來研究下連線物件的獲取。在此之前先了解下DBCP
中幾個Connection
實現類。
類名 | 描述 |
---|---|
DelegatingConnection |
Connection 實現類,是以下幾個類的父類 |
PoolingConnection |
用於包裝原生的Connection ,支援快取prepareStatement 和prepareCall |
PoolableConnection |
用於包裝原生的PoolingConnection (如果沒有開啟poolPreparedStatements ,則包裝的只是原生Connection ),呼叫close() 時只是將連線還給連線池 |
PoolableManagedConnection |
PoolableConnection 的子類,用於包裝ManagedConnection ,支援JTA 和XA 事務 |
ManagedConnection |
用於包裝原生的Connection ,支援JTA 和XA 事務 |
PoolGuardConnectionWrapper |
用於包裝PoolableConnection ,當accessToUnderlyingConnectionAllowed 才能獲取底層連線物件。我們獲取到的就是這個物件 |
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
來實現回收器的定時檢測。 涉及到ThreadPoolExecutor
為JDK
自帶的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
Evictor
是BaseGenericObjectPool
的內部類,實現了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()
這裡的回收過程包括以下四道校驗:
按照
evictionPolicy
校驗idleSoftEvictTime
、idleEvictTime
;利用工廠重新初始化樣本,這裡會校驗
maxConnLifetimeMillis
(testWhileIdle
為true);校驗
maxConnLifetimeMillis
和validationQueryTimeout
(testWhileIdle
為true);校驗所有連線的未使用時間是否超過r
emoveAbandonedTimeout
(removeAbandonedOnMaintenance
為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
獲取PerUserPoolDataSource
和SharedPoolDataSource
物件,選擇使用tomcat 9.0.21
作容器。
如果之前沒有接觸過JNDI
,並不會影響下面例子的理解,其實可以理解為像spring
的bean
配置和獲取。
原始碼分析時已經講到,除了我們熟知的BasicDataSource
,DBCP
還提供了通過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
節點都是我們配置的物件,類似於spring
的bean
節點。其中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&characterEncoding=utf8&serverTimezone=GMT%2B8&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&characterEncoding=utf8&serverTimezone=GMT%2B8&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&characterEncoding=utf8&serverTimezone=GMT%2B8&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;