分布式事务JTA之实践:Spring+ATOMIKOS
本文的目录结构如下:
- 一、概述
- 二、应用场景
- 三、实验模拟需求
- 四、实例测试环境
- 五、源代码下载及配置介绍
- 六、测试验证
一、概述:
本文主要讲述如何基于Atomikos 和spring在项目中实现分布式事务管理
二、应用场景:
如果项目中的数据源来自多个数据库,同时又需要在多数据源中保证事务,此时就需要用到分布式事务处理了。
三、实验模拟需求:
比如有两个对象:用户信息、用户存款,用户信息存在数据库A、存款信息存在数据库B,若客户甲向乙转账,需要在数据库B中对甲、乙的存款信息修改,同时在数据库A中把甲、乙的备注信息最新为最近一次的操作时间。
四、实例测试环境:
- spring、hibernate3.2
- mysql5.1.51(需要版本5.0+)
- AtomikosTransactionsEssentials-3.7.0 (详细可参加它的官网:http://www.atomikos.com )
说明:
1. 测试的数据库需要支持分布式事务,同时JDBC要支持XA连接驱动。本次测试用的mysql5.1是支持事务的,JDBC驱动版本:mysql-connector-java-5.1.7-bin.jar,包含对 XA连接的支持:com.mysql.jdbc.jdbc2.optional.MysqlXAConnection。
2. 附件提供AtomikosTransactionsEssentials 3.7.0 lib包下载:AtomikosTransactionsEssentials-3.7.0-lib.zip。官方下载地址:http://www.atomikos.com/Main/TransactionsEssentialsDownloadForm,需要先注册才能下载。同时这里也提供目前3.7.0的下载链接:http://www.atomikos.com/downloads/transactions-essentials/com/atomikos/AtomikosTransactionsEssentials/3.7.0/AtomikosTransactionsEssentials-3.7.0-bin.zip
五、代码及配置介绍:
源代码下载:分布式事务实例演示源代码michael_jta_code.zip
1.代码的目录结构图如下:
转账测试的的代码片段:
1 |
/** |
2 |
* 转账测试 |
3 |
* @param srcId |
4 |
* @param destId |
5 |
* @param money |
6 |
* @return boolean |
7 |
*/ |
8 |
public boolean doTestTransfer(String srcId, String destId, float money) { |
9 |
10 |
BankAccount srcAccount = bankAccountDao.getByUserName(srcId); |
11 |
BankAccount destAccount = bankAccountDao.getByUserName(destId); |
12 |
if (srcAccount.getDeposit() < money) { |
13 |
System.out.println( "warn :" + srcAccount.getUserName() |
14 |
+ " has not enough money to transfer" ); |
15 |
return false ; |
16 |
} |
17 |
srcAccount.setDeposit(srcAccount.getDeposit() - money); |
18 |
destAccount.setDeposit(destAccount.getDeposit() + money); |
19 |
// 把更新存款信息置于异常发生之前 |
20 |
bankAccountDao.update(srcAccount); |
21 |
bankAccountDao.update(destAccount); |
22 |
23 |
Date curTime = new Date(); |
24 |
UserInfo srcUser = userInfoDao.getById(srcId); |
25 |
UserInfo destUser = userInfoDao.getById(destId); |
26 |
27 |
destUser.setRemark1(curTime + "" ); |
28 |
destUser.setRemark2(curTime + "" ); |
29 |
// 把更新基本信息置于异常发生之前 |
30 |
userInfoDao.update(destUser); |
31 |
srcUser.setRemark1(curTime + "" ); |
32 |
if (srcAccount.getDeposit() < 18000 ) { |
33 |
throw new RuntimeException( "michael test exception for JTA " ); |
34 |
} |
35 |
srcUser.setRemark2(curTime + "" ); |
36 |
37 |
userInfoDao.update(srcUser); |
38 |
System.out.println( "success done:" + srcAccount.getUserName() |
39 |
+ " transfer ¥" + money + " to " + destAccount.getUserName()); |
40 |
41 |
return true ; |
42 |
} |
2. 配置文件详细介绍:
jta.jdbc.properties
1 |
#see http://www.micmiu.com |
2 |
# eg. for mysql |
3 |
jdbc.SDS.class=com.mysql.jdbc.jdbc2.optional.MysqlXADataSource |
4 |
jdbc.SDS.properties=URL=jdbc:mysql://192.168.8.253:3306/demota;user=root;password=111111 |
5 |
6 |
jdbc.SDS2.class=com.mysql.jdbc.jdbc2.optional.MysqlXADataSource |
7 |
jdbc.SDS2.properties=URL=jdbc:mysql://192.168.8.150:3306/demota;user=root;password=111111 |
jta.properties
1 |
#see http://www.micmiu.com |
2 |
com.atomikos.icatch.service=com.atomikos.icatch.standalone.UserTransactionServiceFactory |
3 |
com.atomikos.icatch.console_file_name = tm.out |
4 |
com.atomikos.icatch.log_base_name = tmlog |
5 |
com.atomikos.icatch.tm_unique_name = com.atomikos.spring.jdbc.tm |
6 |
com.atomikos.icatch.console_log_level = INFO |
jta1.hibernate.cfg.xml
1 |
<!DOCTYPE hibernate-configuration |
2 |
PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN" |
4 |
5 |
< hibernate-configuration > |
6 |
< session-factory > |
7 |
< property name = "dialect" > |
8 |
org.hibernate.dialect.MySQL5Dialect |
9 |
</ property > |
10 |
< property name = "hbm2ddl.auto" >update</ property > |
11 |
< mapping class = "michael.jta.atomikos.domain.UserInfo" /> |
12 |
</ session-factory > |
13 |
14 |
</ hibernate-configuration > |
jta2.hibernate.cfg.xml
1 |
<!DOCTYPE hibernate-configuration |
2 |
PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN" |
4 |
5 |
< hibernate-configuration > |
6 |
7 |
< session-factory > |
8 |
< property name = "show_sql" >true</ property > |
9 |
< property name = "dialect" > |
10 |
org.hibernate.dialect.MySQL5Dialect |
11 |
</ property > |
12 |
< property name = "hbm2ddl.auto" >update</ property > |
13 |
< mapping class = "michael.jta.atomikos.domain.BankAccount" /> |
14 |
</ session-factory > |
15 |
16 |
</ hibernate-configuration > |
jta.spring.xml
1 |
<? xml version = "1.0" encoding = "UTF-8" ?> |
2 |
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> |
3 |
< beans > |
4 |
<!-- Configurer that replaces ${...} placeholders with values from properties files --> |
5 |
< bean id = "propertyConfigurer" |
6 |
class = "org.springframework.beans.factory.config.PropertyPlaceholderConfigurer" > |
7 |
< property name = "locations" > |
8 |
< list > |
9 |
< value >classpath:jta.jdbc.properties</ value > |
10 |
</ list > |
11 |
</ property > |
12 |
</ bean > |
13 |
14 |
<!-- 数据源配置 http://sjsky.iteye.com--> |
15 |
< bean id = "SDS" class = "com.atomikos.jdbc.SimpleDataSourceBean" |
16 |
init-method = "init" destroy-method = "close" > |
17 |
< property name = "uniqueResourceName" > |
18 |
< value >SDS</ value > |
19 |
</ property > |
20 |
< property name = "xaDataSourceClassName" > |
21 |
< value >${jdbc.SDS.class}</ value > |
22 |
</ property > |
23 |
< property name = "xaDataSourceProperties" > |
24 |
< value >${jdbc.SDS.properties}</ value > |
25 |
</ property > |
26 |
< property name = "exclusiveConnectionMode" > |
27 |
< value >true</ value > |
28 |
</ property > |
29 |
< property name = "connectionPoolSize" > |
30 |
< value >3</ value > |
31 |
</ property > |
32 |
< property name = "validatingQuery" > |
33 |
< value >SELECT 1</ value > |
34 |
</ property > |
35 |
</ bean > |
36 |
37 |
< bean id = "SDS2" class = "com.atomikos.jdbc.SimpleDataSourceBean" |
38 |
init-method = "init" destroy-method = "close" > |
39 |
< property name = "uniqueResourceName" > |
40 |
< value >SDS2</ value > |
41 |
</ property > |
42 |
< property name = "xaDataSourceClassName" > |
43 |
< value >${jdbc.SDS2.class}</ value > |
44 |
</ property > |
45 |
< property name = "xaDataSourceProperties" > |
46 |
< value >${jdbc.SDS2.properties}</ value > |
47 |
</ property > |
48 |
< property name = "exclusiveConnectionMode" > |
49 |
< value >true</ value > |
50 |
</ property > |
51 |
< property name = "connectionPoolSize" > |
52 |
< value >3</ value > |
53 |
</ property > |
54 |
< property name = "validatingQuery" > |
55 |
< value >SELECT 1</ value > |
56 |
</ property > |
57 |
</ bean > |
58 |
59 |
<!-- sessionFactory http://www.micmiu.com--> |
60 |
< bean id = "sessionFactory1" |
61 |
class = "org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean" > |
62 |
< property name = "dataSource" ref = "SDS" /> |
63 |
< property name = "configLocation" |
64 |
value = "classpath:jta1.hibernate.cfg.xml" /> |
65 |
</ bean > |
66 |
67 |
< bean id = "sessionFactory2" |
68 |
class = "org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean" > |
69 |
< property name = "dataSource" ref = "SDS2" /> |
70 |
< property name = "configLocation" |
71 |
value = "classpath:jta2.hibernate.cfg.xml" /> |
72 |
</ bean > |
73 |
74 |
<!-- TransactionManager http://www.micmiu.com--> |
75 |
<!-- Construct Atomikos UserTransactionManager, needed to configure Spring --> |
76 |
< bean id = "atomikosTransactionManager" |
77 |
class = "com.atomikos.icatch.jta.UserTransactionManager" |
78 |
init-method = "init" destroy-method = "close" > |
79 |
<!-- when close is called, should we force transactions to terminate or not? --> |
80 |
< property name = "forceShutdown" > |
81 |
< value >true</ value > |
82 |
</ property > |
83 |
</ bean > |
84 |
85 |
<!-- Also use Atomikos UserTransactionImp, needed to configure Spring --> |
86 |
< bean id = "atomikosUserTransaction" |
87 |
class = "com.atomikos.icatch.jta.UserTransactionImp" > |
88 |
< property name = "transactionTimeout" > |
89 |
< value >300</ value > |
90 |
</ property > |
91 |
</ bean > |
92 |
93 |
<!-- Configure the Spring framework to use JTA transactions from Atomikos --> |
94 |
< bean id = "springJTATransactionManager" |
95 |
class = "org.springframework.transaction.jta.JtaTransactionManager" > |
96 |
97 |
< property name = "transactionManager" > |
98 |
< ref bean = "atomikosTransactionManager" /> |
99 |
</ property > |
100 |
< property name = "userTransaction" > |
101 |
< ref bean = "atomikosUserTransaction" /> |
102 |
</ property > |
103 |
</ bean > |
104 |
105 |
<!-- Configure DAO http://www.micmiu.com--> |
106 |
< bean id = "userInfoDao" |
107 |
class = "michael.jta.atomikos.dao.impl.UserInfoDaoImpl" > |
108 |
< property name = "sessionFactory" ref = "sessionFactory1" /> |
109 |
</ bean > |
110 |
< bean id = "bankAccountDao" |
111 |
class = "michael.jta.atomikos.dao.BankAccountDao" > |
112 |
< property name = "sessionFactory" ref = "sessionFactory2" /> |
113 |
</ bean > |
114 |
115 |
< bean id = "bankAccountService" |
116 |
class = "michael.jta.atomikos.service.impl.BankAccountServiceImpl" > |
117 |
< property name = "userInfoDao" ref = "userInfoDao" /> |
118 |
< property name = "bankAccountDao" ref = "bankAccountDao" /> |
119 |
</ bean > |
120 |
121 |
<!-- 定义事务规则的拦截器 http://www.micmiu.com--> |
122 |
< bean id = "transactionInterceptor" |
123 |
class = "org.springframework.transaction.interceptor.TransactionInterceptor" > |
124 |
< property name = "transactionManager" |
125 |
ref = "springJTATransactionManager" /> |
126 |
< property name = "transactionAttributes" > |
127 |
< props > |
128 |
< prop key = "*" >PROPAGATION_REQUIRED</ prop > |
129 |
</ props > |
130 |
</ property > |
131 |
</ bean > |
132 |
133 |
<!-- 声明式事务边界配置 所有的bean公用一个代理bean http://sjsky.iteye.com--> |
134 |
< bean id = "baseTransactionProxy" |
135 |
class = "org.springframework.transaction.interceptor.TransactionProxyFactoryBean" |
136 |
abstract = "true" > |
137 |
< property name = "transactionManager" |
138 |
ref = "springJTATransactionManager" /> |
139 |
< property name = "transactionAttributes" > |
140 |
< props > |
141 |
<!-- 可以根据实际情况细化配置提高性能 --> |
142 |
< prop key = "*" >PROPAGATION_REQUIRED</ prop > |
143 |
</ props > |
144 |
</ property > |
145 |
</ bean > |
146 |
147 |
< bean id = "bankAccountServiceProxy" parent = "baseTransactionProxy" > |
148 |
< property name = "target" > |
149 |
< ref bean = "bankAccountService" /> |
150 |
</ property > |
151 |
</ bean > |
152 |
153 |
</ beans > |
六、测试验证
1. 初始化数据:
因为mysql数据库表的类型有事务和非事务之分,建表时一定要注意确保表的类型是事务控制的:InnoDB
数据库A(192.168.8.253):
1 |
DROP DATABASE IF EXISTS demota; |
2 |
CREATE DATABASE demota; |
3 |
USE demota; |
4 |
DROP TABLE IF EXISTS tb_user_info; |
5 |
6 |
CREATE TABLE tb_user_info ( |
7 |
user_name varchar (20), |
8 |
real_name varchar (10), |
9 |
remark1 varchar (50), |
10 |
remark2 varchar (50) |
11 |
) ENGINE = InnoDB; |
12 |
13 |
INSERT INTO tb_user_info (user_name,real_name,remark1,remark2) VALUES ( 'husband' , 'husband' , '' , '' ); |
14 |
INSERT INTO tb_user_info (user_name,real_name,remark1,remark2) VALUES ( 'wife' , 'wife' , '' , '' ); |
数据库B(192.168.8.150):
1 |
DROP DATABASE IF EXISTS demota; |
2 |
CREATE DATABASE demota; |
3 |
USE demota; |
4 |
DROP TABLE IF EXISTS tb_account; |
5 |
CREATE TABLE tb_account ( |
6 |
id int AUTO_INCREMENT, |
7 |
user_name varchar (20), |
8 |
deposit float (10,2), |
9 |
PRIMARY KEY (id) |
10 |
) ENGINE = InnoDB; |
11 |
12 |
INSERT INTO tb_account (user_name,deposit) VALUES ( 'husband' ,20000.00); |
13 |
INSERT INTO tb_account (user_name,deposit) VALUES ( 'wife' ,10000.00); |
2. 测试过程:
ps: 代码中模拟了异常出现的条件:如果账户金额<18000会抛出异常
JtaRunMainTest.java
1 |
package michael.jta.atomikos; |
2 |
3 |
import michael.jta.atomikos.service.BankAccountService; |
4 |
5 |
import org.springframework.context.ApplicationContext; |
6 |
import org.springframework.context.support.ClassPathXmlApplicationContext; |
7 |
8 |
/** |
9 |
* @author michael |
10 |
* |
11 |
*/ |
12 |
public class JtaRunMainTest { |
13 |
14 |
/** |
15 |
* @param args |
16 |
*/ |
17 |
public static void main(String[] args) { |
18 |
System.out.println( "------------start" ); |
19 |
ApplicationContext appCt = new ClassPathXmlApplicationContext( |
20 |
"jta.spring.xml" ); |
21 |
System.out.println( "------------finished init xml" ); |
22 |
23 |
Object bean = appCt.getBean( "bankAccountServiceProxy" ); |
24 |
System.out.println(bean.getClass()); |
25 |
BankAccountService service = (BankAccountService) bean; |
26 |
service.doTestTransfer( "husband" , "wife" , 2000 ); |
27 |
} |
28 |
} |
运行第一次结果:
1 |
------------start |
2 |
------------finished init xml |
3 |
class $Proxy11 |
4 |
Hibernate: select bankaccoun0_. id as id2_, bankaccoun0_.deposit as deposit2_, bankaccoun0_.user_name as user3_2_ from tb_account bankaccoun0_ where bankaccoun0_.user_name=? |
5 |
Hibernate: select bankaccoun0_. id as id2_, bankaccoun0_.deposit as deposit2_, bankaccoun0_.user_name as user3_2_ from tb_account bankaccoun0_ where bankaccoun0_.user_name=? |
6 |
success done :husband transfer ¥2000.0 to wife |
7 |
Hibernate: update tb_account set deposit=?, user_name=? where id =? |
8 |
Hibernate: update tb_account set deposit=?, user_name=? where id =? |
运行第二次结果:
1 |
------------start |
2 |
------------finished init xml |
3 |
class $Proxy11 |
4 |
Hibernate: select bankaccoun0_. id as id2_, bankaccoun0_.deposit as deposit2_, bankaccoun0_.user_name as user3_2_ from tb_account bankaccoun0_ where bankaccoun0_.user_name=? |
5 |
Hibernate: select bankaccoun0_. id as id2_, bankaccoun0_.deposit as deposit2_, bankaccoun0_.user_name as user3_2_ from tb_account bankaccoun0_ where bankaccoun0_.user_name=? |
6 |
Exception in thread "main" java.lang.RuntimeException: michael test exception for JTA |
7 |
at michael.jta.atomikos.service.impl.BankAccountServiceImpl.doTestTransfer(BankAccountServiceImpl.java:51) |
8 |
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) |
9 |
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) |
10 |
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) |
11 |
at java.lang.reflect.Method.invoke(Method.java:597) |
12 |
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:299) |
13 |
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:172) |
14 |
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:139) |
15 |
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:107) |
16 |
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:161) |
17 |
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:202) |
18 |
at $Proxy11.doTestTransfer(Unknown Source) |
19 |
at michael.jta.atomikos.JtaRunMainTest.main(JtaRunMainTest.java:26) |
测试过程中数据库查询的结果截图:
从上面的数据库截图可见,第一正常运行时两个数据库同步更新了,第二次运行发生异常后,两个数据库的数据为发生变化,实现了事务回滚。
原创文章,转载请注明: 转载自micmiu – 大大的技术 | 小小的生活[ http://www.micmiu.com/ ]
本文链接地址: http://www.micmiu.com/j2ee/spring/jta-spring-atomikos/