單體應用演進至微服務/分庫分表階段,跨多個數據庫的業務操作(如訂單服務同時寫入「訂單庫」與「庫存庫」)要求原子性:要麼全成功,要麼全回滾。傳統 JDBC 事務綁定單一 Connection,無法跨數據源。JTA(Java Transaction API)定義了事務管理器與資源管理器(XADataSource)的標準協議,支持全局事務協調多個 XA 資源的兩階段提交(2PC)。
本方案採用 Spring 声明式事務 + JOTM 嵌入式事務管理器 + iBatis/MyBatis + XAPool 數據源池,適用於不依賴應用服務器(如 WebLogic、JBoss)的獨立部署場景。
<!-- JOTM: Java Open Transaction Manager -->
<dependency>
<groupId>org.ow2.jotm</groupId>
<artifactId>jotm</artifactId>
<version>2.2.1</version>
</dependency>
<!-- XAPool: XA 連接池 -->
<dependency>
<groupId>org.enhydra.jdbc</groupId>
<artifactId>xapool</artifactId>
<version>1.5.0</version>
</dependency>
<!-- MyBatis-Spring 整合 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.6</version>
</dependency>
Spring 3 移除了 JotmFactoryBean,需自行實現 FactoryBean + DisposableBean 管理 JOTM 生命週期:
public class JotmFactoryBean implements FactoryBean<Current>, DisposableBean {
private Current jotmCurrent;
private Jotm jotm;
public JotmFactoryBean() throws NamingException {
jotmCurrent = Current.getCurrent();
if (jotmCurrent == null) {
jotm = new Jotm(true, false); // 獨立模式,不綁定 JNDI
jotmCurrent = Current.getCurrent();
}
}
public void setDefaultTimeout(int timeout) { ... }
public Current getObject() { return jotmCurrent; }
public Class<?> getObjectType() { return Current.class; }
public boolean isSingleton() { return true; }
public void destroy() { if (jotm != null) jotm.stop(); }
}
封裝 SqlSessionTemplate 通用 CRUD:queryForObject、queryForList、insert/update/delete、getTotalCount。關鍵方法暴露 setSqlSession(Session) 供運行時動態切換數據源。
持有多個 SqlSession(dmcWeb、idxWeb、rptWeb)與對應的數據源鍵映射。核心邏輯:
ResourceBundle 讀取 mybaties.properties 映射 sqlId → dataSourceKeygetSqlSession(sqlId) 根據語句 ID 解析目標數據源,返回對應 SessionJtaTransactionManager.getUserTransaction() 手動控制全局事務邊界:begin() → 多源操作 → commit() / rollback()
public int[] update(Object object, String... sqlIds) {
UserTransaction tx = transactionManager.getUserTransaction();
try {
tx.begin();
int[] results = new int[sqlIds.length];
for (int i = 0; i < sqlIds.length; i++) {
baseDao.setSqlSession(getSqlSession(sqlIds[i]));
results[i] = baseDao.update(object, sqlIds[i]);
}
tx.commit();
return results;
} catch (Exception e) {
try { tx.rollback(); } catch (Exception ignored) {}
throw new RuntimeException(e);
}
}
<!-- 1. JOTM 事務管理器 -->
<bean id="jotm" class="com.ai.dimensyn.dynamicDataSoure.JotmFactoryBean" />
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="userTransaction" ref="jotm" />
</bean>
<!-- 2. XA 數據源(每個庫一套) -->
<bean id="dataSourceDmcWeb" class="org.enhydra.jdbc.pool.StandardXAPoolDataSource" destroy-method="shutdown">
<property name="dataSource">
<bean class="org.enhydra.jdbc.standard.StandardXADataSource" destroy-method="shutdown">
<property name="transactionManager" ref="jotm" />
<property name="driverName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://ip:port/dmc?useUnicode=true&characterEncoding=utf-8" />
<property name="user" value="xxx" />
<property name="password" value="xxx" />
</bean>
</property>
</bean>
<!-- dataSourceIdxWeb、dataSourceRptWeb 同理 -->
<!-- 3. SqlSessionFactory & SqlSessionTemplate -->
<bean id="dmcWebSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="configLocation" value="classpath:orm/Configuration.xml" />
<property name="dataSource" ref="dataSourceDmcWeb" />
</bean>
<bean id="dmcWebSession" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="dmcWebSessionFactory" />
</bean>
<!-- 4. 多数据源 DAO 注入所有 Session -->
<bean id="baseMDao" class="com.ai.dimensyn.dao.common.impl.BaseMDaoImpl">
<property name="transactionManager" ref="transactionManager" />
<property name="dmcWebSession" ref="dmcWebSession" />
<property name="dmcWebSrcKey" value="dmcWeb" />
<property name="idxWebSession" ref="idxWebSession" />
<property name="idxWebSrcKey" value="indexWeb" />
<property name="rptWebSession" ref="rptWebSession" />
<property name="rptWebSrcKey" value="reportWeb" />
<property name="defaultSrcKey" value="dmcWeb" />
</bean>
<!-- mybaties.properties: sqlId 前綴映射數據源 -->
dimensyn.deleteDimenTable-testtable_xuchun=indexWeb
dimensyn.insertDimenTable-testtable_xuchun=dmcWeb
<!-- dimensynMapper.xml -->
<mapper namespace="dimensyn">
<delete id="deleteDimenTable" parameterType="java.util.Map">
delete from ${table_tb}
</delete>
<insert id="insertDimenTable" parameterType="java.util.Map">
-- 動態 SQL 由應用層組裝
</insert>
</mapper>
運行時 BaseMDaoImpl 解析 sqlId(如 deleteDimenTable-testtable_xuchun),取前綴 deleteDimenTable 查找 properties 獲取 indexWeb,進而路由到 idxWebSession。
// 業務服務注入 baseMDao
public void testDistributedTx() {
Map<String, String> params = new HashMap<>();
params.put("table_tb", "test_table");
params.put(Constants.SQL, "insert into ...; insert into ...;"); // 多條 SQL 以分號分隔
baseMDao.executeDeleteInsert(params,
"dimensyn.deleteDimenTable-testtable_xuchun",
"dimensyn.insertDimenTable-testtable_xuchun");
}
關鍵避坑指南:
InnoDB 引擎支持 XA;mysql-connector-java 版本 ≥ 5.1.38(修復 XA 相關 Bug)jotm.setDefaultTimeout()UserTransaction 時,確保同一線程內多次操作共享同一全局事務上下文