當業務涉及多個數據庫,且要求跨庫操作具備原子性(要麼全成功,要麼全回滾)時,本地事務無法滿足需求,必須引入 JTA(Java Transaction API)分布式事務。本文詳細記錄 Spring 整合 ObjectWeb JOTM 實現跨數據庫 JTA 事務的完整配置與驗證過程。
將以下 JAR 放入類路徑:
jotm.jar - JOTM 核心xapool.jar - XA 連接池jotm_jrmp_stubs.jar - RMI 存根jta-spec1_0_1.jar - JTA 規範 APIconnector-1_5.jar - JCA 連接器規範mysql-connector-java 5.1+)創建 carol.properties 置於類路徑根目錄:
# JNDI 協議
carol.protocols=jrmp
# 不使用 CAROL JNDI 包裝器
carol.start.jndi=false
# 不啟動命名服務
carol.start.ns=false
創建兩個測試庫 jtatesta、jtatestb,各建一表:
-- jtatesta
CREATE TABLE `user` (
`user_id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`user_name` VARCHAR(45) NOT NULL,
`user_password` VARCHAR(45) NOT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- jtatestb
CREATE TABLE `grade` (
`grade_id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` INT(10) UNSIGNED NOT NULL,
`grade` DOUBLE NOT NULL,
PRIMARY KEY (`grade_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
關鍵點:表引擎必須為 InnoDB,MyISAM 不支援事務。
// User.java
public class User implements Serializable {
private int userId;
private String userName;
private String userPassword;
// getter/setter 省略
}
// Grade.java
public class Grade implements Serializable {
private int gradeId;
private User user;
private double grade;
// getter/setter 省略
}
// UserJdbcDao.java
@Repository
public class UserJdbcDao extends JdbcDaoSupport implements UserDao {
public void addUser(User user) {
String sql = "INSERT INTO user(user_id, user_name, user_password) VALUES(?,?,?)";
getJdbcTemplate().update(sql, user.getUserId(), user.getUserName(), user.getUserPassword());
}
}
// GradeJdbcDao.java
@Repository
public class GradeJdbcDao extends JdbcDaoSupport implements GradeDao {
public void addGrade(Grade grade) {
String sql = "INSERT INTO grade(user_id, grade) VALUES(?,?)";
getJdbcTemplate().update(sql, grade.getUser().getUserId(), grade.getGrade());
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://www.springframework.org/schema/beans"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="https://www.springframework.org/schema/aop"
xmlns:tx="https://www.springframework.org/schema/tx"
xsi:schemaLocation="
https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
https://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd
https://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- JOTM 本地實例 -->
<bean id="jotm" class="org.springframework.transaction.jta.JotmFactoryBean"/>
<!-- JTA 事務管理器 -->
<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="userTransaction" ref="jotm"/>
</bean>
<!-- XA 數據源 A:jtatesta -->
<bean id="jtaTestADS" 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://localhost/jtatesta"/>
</bean>
</property>
<property name="user" value="root"/>
<property name="password" value="885123"/>
</bean>
<!-- XA 數據源 B:jtatestb -->
<bean id="jtaTestBDS" 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://localhost/jtatestb"/>
</bean>
</property>
<property name="user" value="root"/>
<property name="password" value="885123"/>
</bean>
<!-- JdbcTemplate 綁定對應 XA 數據源 -->
<bean id="jtaTestATemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="jtaTestADS"/>
</bean>
<bean id="jtaTestBTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="jtaTestBDS"/>
</bean>
<!-- DAO 注入對應 Template -->
<bean id="userDao" class="com.dao.jdbc.UserJdbcDao">
<property name="jdbcTemplate" ref="jtaTestATemplate"/>
</bean>
<bean id="gradeDao" class="com.dao.jdbc.GradeJdbcDao">
<property name="jdbcTemplate" ref="jtaTestBTemplate"/>
</bean>
<!-- 業務服務 -->
<bean id="myService" class="com.service.impl.MyServiceImpl">
<property name="userDao" ref="userDao"/>
<property name="gradeDao" ref="gradeDao"/>
</bean>
<!-- 註解驅動事務 -->
<tx:annotation-driven transaction-manager="txManager" proxy-target-class="true"/>
</beans>
public class TestMain {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx =
new ClassPathXmlApplicationContext("app_jta.xml");
MyService ms = ctx.getBean("myService", MyService.class);
User user = new User();
user.setUserId(1); // 故意設置重複主鍵
user.setUserName("tufu");
user.setUserPassword("tufu");
Grade grade = new Grade();
grade.setGrade(100);
grade.setUser(user);
ms.addGrade(user, grade); // 預期:user 插入失敗,grade 同步回滾
}
}
log4j.rootLogger=DEBUG, console 觀察 JTA 事務邊界、XAResource 登記、兩階段提交/回滾日誌user、grade 表,應均無新增記錄DataIntegrityViolationException(主鍵衝突),事務管理器捕獲後觸發全局回滾?pinGlobalTxToPhysicalConnection=trueStandardXAPoolDataSource 的 destroy-method="shutdown" 必須配置,否則應用關閉時連接池不釋放transactionManager 屬性必須引用同一 jotm 實例,確保同一全局事務上下文通過 Spring 的 JotmFactoryBean 與 JtaTransactionManager,配合 XAPool 封裝 XADataSource,我們以最小依賴實現了輕量級的跨庫分布式事務。這套方案適合中小型應用、開發測試環境,或作為理解 JTA 原理的入門範例。