Springboot+Atomikos+Jpa+Mysql实现JTA分布式事务
1 前言
之前整理了一个spring+jotm实现的分布式事务实现,但是听说spring3.X后不再支持jotm了,jotm也有好几年没更新了,所以今天整理springboot+Atomikos+jpa+mysql的JTA分布式事务实现。
Atomikos网上的资料确实比jotm多,另外我发现STS工具里集成了Atomikos,那spring对Atomikos的支持毋庸置疑肯定会在相当长的时间内会是友好的。
2 开发环境
Springboot 1.0.1 + Atomikos 3.9.3 + JPA (Hibernate 4.3.5) + Mysql 5.1.73 + Mysql Connector 5.1.31 + Junit + Maven
3 代码
这套代码的基础我是从网上下载的,作了些修改,因为它原来用的是H2数据库,我改成了Mysql,另外,其实spring官网的例子也是这样写的,http://spring.io/blog/2011/08/15/configuring-spring-and-jta-without-full-java-ee/
3.1 数据库sql
1 DROP DATABASE IF EXISTS `datasource1`; 2 CREATE DATABASE `datasource1` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; 3 4 use datasource1; 5 6 DROP TABLE IF EXISTS `orders`; 7 CREATE TABLE `orders` ( 8 `id` int(11) NOT NULL AUTO_INCREMENT, 9 `code` int(11) DEFAULT NULL, 10 `quantity` int(11) DEFAULT NULL, 11 PRIMARY KEY (`id`) 12 ) ENGINE=MyISAM DEFAULT CHARSET=utf8; 13 14 DROP DATABASE IF EXISTS `datasource2`; 15 CREATE DATABASE `datasource2` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; 16 17 use datasource2; 18 19 DROP TABLE IF EXISTS `customer`; 20 CREATE TABLE `customer` ( 21 `id` int(11) NOT NULL AUTO_INCREMENT, 22 `name` varchar(45) DEFAULT NULL, 23 `age` int(11) DEFAULT NULL, 24 PRIMARY KEY (`id`) 25 ) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
3.2 部分重要代码
pom.xml
1 <dependencies> 2 <dependency> 3 <groupId>org.springframework.boot</groupId> 4 <artifactId>spring-boot-starter-data-jpa</artifactId> 5 </dependency> 6 7 <!-- <dependency> 8 <groupId>com.h2database</groupId> 9 <artifactId>h2</artifactId> 10 </dependency> --> 11 12 <dependency> 13 <groupId>org.projectlombok</groupId> 14 <artifactId>lombok</artifactId> 15 <version>1.12.4</version> 16 </dependency> 17 18 <dependency> 19 <groupId>com.atomikos</groupId> 20 <artifactId>transactions</artifactId> 21 <version>3.9.3</version> 22 </dependency> 23 24 <dependency> 25 <groupId>com.atomikos</groupId> 26 <artifactId>transactions-jta</artifactId> 27 <version>3.9.3</version> 28 </dependency> 29 30 <dependency> 31 <groupId>com.atomikos</groupId> 32 <artifactId>transactions-hibernate3</artifactId> 33 <version>3.9.3</version> 34 <exclusions> 35 <exclusion> 36 <artifactId>hibernate</artifactId> 37 <groupId>org.hibernate</groupId> 38 </exclusion> 39 </exclusions> 40 </dependency> 41 42 <dependency> 43 <groupId>org.springframework.boot</groupId> 44 <artifactId>spring-boot-starter-test</artifactId> 45 </dependency> 46 47 <dependency> 48 <groupId>junit</groupId> 49 <artifactId>junit</artifactId> 50 <scope>test</scope> 51 </dependency> 52 53 <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --> 54 <dependency> 55 <groupId>mysql</groupId> 56 <artifactId>mysql-connector-java</artifactId> 57 <version>5.1.31</version> 58 </dependency> 59 60 </dependencies>
application.properties
1 spring.main.show_banner=false 2 3 order.datasource.url=jdbc:mysql://192.168.0.12:3306/datasource1?serverTimezone=UTC 4 order.datasource.user=root 5 order.datasource.password=123456 6 #jdbc:h2:order 7 8 customer.datasource.url=jdbc:mysql://127.0.0.1:3312/datasource2?serverTimezone=UTC 9 customer.datasource.user=root 10 customer.datasource.password=123456
1 import java.util.HashMap; 2 3 import javax.sql.DataSource; 4 5 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.boot.context.properties.EnableConfigurationProperties; 7 import org.springframework.context.annotation.Bean; 8 import org.springframework.context.annotation.Configuration; 9 import org.springframework.context.annotation.DependsOn; 10 import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 11 import org.springframework.orm.jpa.JpaVendorAdapter; 12 import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; 13 14 import com.at.mul.repository.customer.CustomerDatasourceProperties; 15 import com.atomikos.jdbc.AtomikosDataSourceBean; 16 import com.mysql.jdbc.jdbc2.optional.MysqlXADataSource; 17 18 @Configuration 19 @DependsOn("transactionManager") 20 @EnableJpaRepositories(basePackages = "com.at.mul.repository.customer", entityManagerFactoryRef = "customerEntityManager", transactionManagerRef = "transactionManager") 21 @EnableConfigurationProperties(CustomerDatasourceProperties.class) 22 public class CustomerConfig { 23 24 @Autowired 25 private JpaVendorAdapter jpaVendorAdapter; 26 27 @Autowired 28 private CustomerDatasourceProperties customerDatasourceProperties; 29 30 @Bean(name = "customerDataSource", initMethod = "init", destroyMethod = "close") 31 public DataSource customerDataSource() { 32 MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource(); 33 mysqlXaDataSource.setURL(customerDatasourceProperties.getUrl()); 34 mysqlXaDataSource.setUser(customerDatasourceProperties.getUser()); 35 mysqlXaDataSource.setPassword(customerDatasourceProperties.getPassword()); 36 mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true); 37 38 AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean(); 39 xaDataSource.setXaDataSource(mysqlXaDataSource); 40 xaDataSource.setUniqueResourceName("datasource2"); 41 // xaDataSource.setXaDataSourceClassName("com.mysql.jdbc.jdbc2.optional.MysqlXADataSource"); 42 return xaDataSource; 43 } 44 45 @Bean(name = "customerEntityManager") 46 @DependsOn("transactionManager") 47 public LocalContainerEntityManagerFactoryBean customerEntityManager() throws Throwable { 48 49 HashMap<String, Object> properties = new HashMap<String, Object>(); 50 properties.put("hibernate.transaction.jta.platform", AtomikosJtaPlatform.class.getName()); 51 properties.put("javax.persistence.transactionType", "JTA"); 52 53 LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean(); 54 entityManager.setJtaDataSource(customerDataSource()); 55 entityManager.setJpaVendorAdapter(jpaVendorAdapter); 56 entityManager.setPackagesToScan("com.at.mul.domain.customer"); 57 entityManager.setPersistenceUnitName("customerPersistenceUnit"); 58 entityManager.setJpaPropertyMap(properties); 59 return entityManager; 60 } 61 62 }
1 @Configuration 2 @DependsOn("transactionManager") 3 @EnableJpaRepositories(basePackages = "com.at.mul.repository.order", entityManagerFactoryRef = "orderEntityManager", transactionManagerRef = "transactionManager") 4 @EnableConfigurationProperties(OrderDatasourceProperties.class) 5 public class OrderConfig { 6 7 @Autowired 8 private JpaVendorAdapter jpaVendorAdapter; 9 10 @Autowired 11 private OrderDatasourceProperties orderDatasourceProperties; 12 13 @Bean(name = "orderDataSource", initMethod = "init", destroyMethod = "close") 14 public DataSource orderDataSource() { 15 MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource(); 16 mysqlXaDataSource.setURL(orderDatasourceProperties.getUrl()); 17 mysqlXaDataSource.setUser(orderDatasourceProperties.getUser()); 18 mysqlXaDataSource.setPassword(orderDatasourceProperties.getPassword()); 19 // mysqlXaDataSource.setAllowMultiQueries(true); 20 // mysqlXaDataSource.setLogXaCommands(true); 21 mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true); 22 23 AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean(); 24 xaDataSource.setXaDataSource(mysqlXaDataSource); 25 xaDataSource.setUniqueResourceName("datasource1"); 26 // xaDataSource.setXaDataSourceClassName("com.mysql.jdbc.jdbc2.optional.MysqlXADataSource"); 27 return xaDataSource; 28 } 29 30 @Bean(name = "orderEntityManager") 31 public LocalContainerEntityManagerFactoryBean orderEntityManager() throws Throwable { 32 33 HashMap<String, Object> properties = new HashMap<String, Object>(); 34 properties.put("hibernate.transaction.jta.platform", AtomikosJtaPlatform.class.getName()); 35 properties.put("javax.persistence.transactionType", "JTA"); 36 37 LocalContainerEntityManagerFactoryBean entityManager = new LocalContainerEntityManagerFactoryBean(); 38 entityManager.setJtaDataSource(orderDataSource()); 39 entityManager.setJpaVendorAdapter(jpaVendorAdapter); 40 entityManager.setPackagesToScan("com.at.mul.domain.order"); 41 entityManager.setPersistenceUnitName("orderPersistenceUnit"); 42 entityManager.setJpaPropertyMap(properties); 43 return entityManager; 44 } 45 46 }
1 import com.at.mul.domain.customer.Customer; 2 import com.at.mul.domain.order.Order; 3 import com.at.mul.exception.NoRollbackException; 4 import com.at.mul.exception.StoreException; 5 6 public interface StoreService { 7 8 void store(Customer customer, Order order) throws Exception; 9 10 void storeWithStoreException(Customer customer, Order order) throws StoreException; 11 12 void storeWithNoRollbackException(Customer customer, Order order) throws NoRollbackException; 13 14 }
1 import org.springframework.beans.factory.annotation.Autowired; 2 import org.springframework.stereotype.Service; 3 import org.springframework.transaction.annotation.Transactional; 4 5 import com.at.mul.domain.customer.Customer; 6 import com.at.mul.domain.order.Order; 7 import com.at.mul.exception.NoRollbackException; 8 import com.at.mul.exception.StoreException; 9 import com.at.mul.repository.customer.CustomerRepository; 10 import com.at.mul.repository.order.OrderRepository; 11 12 @Service 13 public class StoreServiceImpl implements StoreService { 14 15 @Autowired 16 private CustomerRepository customerRepository; 17 18 @Autowired 19 private OrderRepository orderRepository; 20 21 @Transactional 22 public void store(Customer customer, Order order) { 23 customerRepository.save(customer); 24 orderRepository.save(order); 25 } 26 27 @Transactional(rollbackFor = StoreException.class) 28 public void storeWithStoreException(Customer customer, Order order) throws StoreException { 29 customerRepository.save(customer); 30 orderRepository.save(order); 31 throw new StoreException(); 32 } 33 34 @Transactional(noRollbackFor = NoRollbackException.class, rollbackFor = StoreException.class) 35 public void storeWithNoRollbackException(Customer customer, Order order) throws NoRollbackException { 36 customerRepository.save(customer); 37 orderRepository.save(order); 38 throw new NoRollbackException(); 39 } 40 41 }
完整代码下载:http://download.csdn.net/download/u013081610/9927514
4 遇到的坑
4.1 bug: The server time zone value '�й���ʱ��' is unrecognized or represents more than one time zone.
http://blog.csdn.net/sunlggggg/article/details/54564114
4.2 com.mysql.jdbc.jdbc2.optional.MysqlXAException: XAER_INVAL: Invalid arguments (or unsupported command)
WARNING: XA resource 'jdbc/mysqlDs': resume for XID '3139322E3136382E31342E3131372E746D30303030323030303831:3139322E3136382E31342E3131372E746D32' raised -5: invalid arguments were given for the XA operation
这个错误是我运行StoreServiceTest里的testStore()方法时出现的,就是把数据分别插入两个库的表里,之前用H2的时候都很正常,但是换成Mysql就是不行,操作第二个库的时候就报这个错,第一个不会报错。
猜测可能是以下原因吧
a.这可能是MySQL服务器对XA支持的限制,也就是可能是MySQL的一个bug,可以看Mysql官方文档的解释https://dev.mysql.com/doc/refman/5.5/en/xa-statements.html
b.也可能是atomikos里的问题,具体看https://www.atomikos.com/Documentation/KnownProblems#ActiveMQ_error:_34Transaction_39XID:..._39_has_not_been_started_34
找到下面的段落
MySQL XA bug Some users have reported problems with MySQL XA (related to this MySQL bug: http://bugs.mysql.com/bug.php?id=27832external). This problem only happens if you access the same MySQL database more than once in the same transaction. A workaround can be setting the following property in jta.properties: com.atomikos.icatch.serial_jta_transactions=false Also, make sure to set the following property on the MySQL datasource: pinGlobalTxToPhysicalConnection="true" MariaDB's java driver also supports this workaround since v.1.1.8
看来atomikos已经针对mysql的这个bug作了处理了,根据提示我加了mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true)就可以了。
4.3 貌似对mysql的InnoDB引擎没用
具体什么原因我还没研究,暂时测试只是使用的MyISAM