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