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

原文地址:https://www.cnblogs.com/shamo89/p/7326718.html