分佈式事務(四)簡單樣例

系列目錄

分佈式事務(一)原理概覽

分佈式事務(二)JTA規範

分佈式事務(三)mysql對XA協議的支持

分佈式事務(四)簡單樣例

分佈式事務(五)源碼詳解

分佈式事務(六)總結提高

一、引子

1.1 背景

鑑於spring boot滿天飛的大環境,本節樣例使用Spring Boot+Atomikos(TM)+Mybatis(ORM)+Mysql(DB)的架構。

兩張飛機票:

springboot官網文檔:(只到docs即可,瀏覽可查看全部版本文檔,不過都是英文的)

springboot中文文檔:這個是2.0版本的,沒找到1.5.10的。

2.spring boot對jta的支持

翻譯自官網:

  Spring Boot通過AtomkosBitronix的內嵌事務管理器支持跨多個XA資源的分佈式JTA事務,當部署到恰當的J2EE應用服務器時也會支持JTA事務。

當發現JTA環境時,Spring Boot將使用Spring的JtaTransactionManager來管理事務。自動配置的JMS,DataSource和JPA beans將被升級以支持XA事務。你可以使用@Transactional,來參與到一個分佈式事務中。如果處於JTA環境,但仍想使用本地事務,你可以將spring.jta.enabled屬性設置爲false來禁用JTA自動配置功能。

  Atomikos是一個流行的開源事務管理器,並且可以嵌入到你的Spring Boot應用中。你可以使用spring-boot-starter-jta-atomikos去拉取Atomikos庫。

默認情況下,Atomikos事務日誌將被記錄在應用home目錄下的transaction-logs文件夾中。你可以在application.properties文件中通過設置spring.jta.log-dir屬性來定義該目錄,以spring.jta.atomikos.properties開頭的屬性能用來定義Atomikos的UserTransactionServiceIml實現,具體參考AtomikosProperties javadoc

  注:爲了確保多個事務管理器能夠安全地和相應的資源管理器配合,每個Atomikos實例必須設置一個唯一的ID。默認情況下,該ID是Atomikos實例運行的機器上的IP地址。爲了確保生產環境中該ID的唯一性,你需要爲應用的每個實例設置不同的spring.jta.transaction-manager-id屬性值。

二、簡單樣例

2.1 業務場景

 

業務場景:

2個DB:

  1. test庫中有user用戶表

  2. test2庫中有user_msg用戶備註表

 當插入一條user記錄時,同時插入一條user_msg。如果出現異常,2個庫中的數據都能回滾。

2.2 簡單樣例

1. 導包:pom.xml中導入atomikos包:

1 <dependency>
2     <groupId>org.springframework.boot</groupId>
3     <artifactId>spring-boot-starter-jta-atomikos</artifactId>
4     <version>2.1.3.RELEASE</version>
5 </dependency>

2. 主庫數據源配置

MasterRepositoryConfig

 1 package study.config.datasource;
 2 
 3 import org.apache.ibatis.session.SqlSessionFactory;
 4 import org.mybatis.spring.SqlSessionFactoryBean;
 5 import org.mybatis.spring.SqlSessionTemplate;
 6 import org.mybatis.spring.annotation.MapperScan;
 7 import org.springframework.beans.factory.annotation.Qualifier;
 8 import org.springframework.boot.context.properties.ConfigurationProperties;
 9 import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
10 import org.springframework.context.annotation.Bean;
11 import org.springframework.context.annotation.Configuration;
12 import org.springframework.context.annotation.Primary;
13 import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
14 
15 import javax.sql.DataSource;
16 import java.util.Properties;
17 
18 /**
19  * @author denny
20  * @Description 主庫持久化配置
21  * @date 2019/5/5 下午3:12
22  */
23 @Configuration
24 @MapperScan(basePackages = {MasterRepositoryConfig.MASTER_PACKAGE}, sqlSessionFactoryRef = "masterSqlSessionFactory")
25 public class MasterRepositoryConfig {
26 
27     static final String MASTER_PACKAGE = "study.repository.master";
28 
29     private static final String MAPPER_LOCATIONS = "classpath*:mybatis/mapper/master/**/*.xml";
30 
31     @ConfigurationProperties(prefix = "study.datasource.master")
32     @Bean(name = "masterDataSource")
33     @Primary
34     public DataSource masterDataSource() {
35         // 連接池基本屬性
36         Properties p = new Properties();
37         p.setProperty("url", "jdbc:mysql://localhost:3306/" + "test");
38         p.setProperty("user", "root");
39         p.setProperty("password", "12345");
40 
41         AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
42         ds.setUniqueResourceName("masterDataSource");
43         ds.setXaDataSourceClassName("com.mysql.jdbc.jdbc2.optional.MysqlXADataSource");
44         ds.setXaProperties(p);
45         ds.setPoolSize(5);
46         return ds;
47     }
48 
49     @Bean(name = "masterSqlSessionFactory")
50     @Primary
51     public SqlSessionFactory sqlSessionFactory(@Qualifier("masterDataSource") DataSource dataSource) throws Exception {
52         SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
53         fb.setDataSource(dataSource);
54         //指定基包
55         fb.setTypeAliasesPackage(MASTER_PACKAGE);
56         //指定xml文件位置
57         fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_LOCATIONS));
58         return fb.getObject();
59     }
60 
61     /**
62      * 基於sqlSession的操作模板類
63      *
64      * @param sqlSessionFactory
65      * @return
66      */
67     @Bean(name = "masterSqlSessionTemplate")
68     @Primary
69     public SqlSessionTemplate sqlSessionTemplate(@Qualifier("masterSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
70         SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
71         return sqlSessionTemplate;
72     }
73 }

3.從庫數據源配置

 SlaveRepositoryConfig

 1 package study.config.datasource;
 2 
 3 import org.apache.ibatis.session.SqlSessionFactory;
 4 import org.mybatis.spring.SqlSessionFactoryBean;
 5 import org.mybatis.spring.SqlSessionTemplate;
 6 import org.mybatis.spring.annotation.MapperScan;
 7 import org.springframework.beans.factory.annotation.Qualifier;
 8 import org.springframework.boot.context.properties.ConfigurationProperties;
 9 import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
10 import org.springframework.context.annotation.Bean;
11 import org.springframework.context.annotation.Configuration;
12 import org.springframework.context.annotation.Primary;
13 import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
14 
15 import javax.sql.DataSource;
16 import java.util.Properties;
17 
18 /**
19  * @author denny
20  * @Description 從庫持久化配置
21  * @date 2019/5/5 下午3:12
22  */
23 @Configuration
24 @MapperScan(basePackages = {SlaveRepositoryConfig.MASTER_PACKAGE}, sqlSessionFactoryRef = "slaveSqlSessionFactory")
25 public class SlaveRepositoryConfig {
26 
27     static final String MASTER_PACKAGE = "study.repository.slave";
28 
29     private static final String MAPPER_LOCATIONS = "classpath*:mybatis/mapper/slave/**/*.xml";
30 
31     /**
32      * 從數據源:test2:user_msg表
33      *
34      * @return
35      */
36     @ConfigurationProperties(prefix = "study.datasource.slave")
37     @Bean(name = "slaveDataSource")
38     public DataSource slaveDataSource() {
39         // 連接池基本屬性
40         Properties p = new Properties();
41         p.setProperty("url", "jdbc:mysql://localhost:3306/" + "test2");
42         p.setProperty("user", "root");
43         p.setProperty("password", "12345");
44 
45         AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
46         ds.setUniqueResourceName("slaveDataSource");
47         ds.setXaDataSourceClassName("com.mysql.jdbc.jdbc2.optional.MysqlXADataSource");
48         ds.setXaProperties(p);
49         ds.setPoolSize(5);
50         return ds;
51     }
52 
53     /**
54      * 會話工廠
55      *
56      * @return
57      * @throws Exception
58      */
59     @Bean(name = "slaveSqlSessionFactory")
60     @Primary
61     public SqlSessionFactory sqlSessionFactory(@Qualifier("slaveDataSource") DataSource dataSource) throws Exception {
62         SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
63         fb.setDataSource(dataSource);
64         //指定基包
65         fb.setTypeAliasesPackage(MASTER_PACKAGE);
66         //指定xml文件位置
67         fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_LOCATIONS));
68         return fb.getObject();
69     }
70 
71     /**
72      * 基於sqlSession的操作模板類
73      *
74      * @param sqlSessionFactory
75      * @return
76      * @throws Exception
77      */
78     @Bean(name = "slaveSqlSessionTemplate")
79     @Primary
80     public SqlSessionTemplate sqlSessionTemplate(@Qualifier("slaveSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
81         SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory);
82         return sqlSessionTemplate;
83     }
84 }

4. 配置分佈式事務管理器

 1 package study.config.datasource;
 2 
 3 import com.atomikos.icatch.jta.UserTransactionImp;
 4 import com.atomikos.icatch.jta.UserTransactionManager;
 5 import org.springframework.context.annotation.Bean;
 6 import org.springframework.context.annotation.Configuration;
 7 import org.springframework.transaction.jta.JtaTransactionManager;
 8 
 9 import javax.transaction.UserTransaction;
10 
11 /**
12  * 事務管理器配置類
13  *
14  * @author denny
15  */
16 @Configuration
17 public class JtaTransactionManagerConfig {
18 
19     @Bean(name = "atomikosTransactionManager")
20     public JtaTransactionManager regTransactionManager() {
21         UserTransactionManager userTransactionManager = new UserTransactionManager();
22         UserTransaction userTransaction = new UserTransactionImp();
23         return new JtaTransactionManager(userTransaction, userTransactionManager);
24     }
25 }

5. addUser方法

 1 package study.service.impl;
 2 
 3 import lombok.extern.slf4j.Slf4j;
 4 import org.springframework.stereotype.Service;
 5 import study.domain.User;
 6 import study.repository.master.UserRepository;
 7 import study.service.UserService;
 8 
 9 import javax.annotation.Resource;
10 
11 /**
12  * @Description 
13  * @author denny
14  * @date 2018/8/27 下午5:31
15  */
16 @Slf4j
17 @Service
18 public class UserServiceImpl implements UserService{
19     @Resource
20     private UserRepository userRepository;
21 
22     //@Transactional(propagation= Propagation.REQUIRED, rollbackFor = Exception.class)
23     @Override
24     public void addUser(int id, String name) {
25         log.info("[addUser] begin!!!");
26         User user = new User();
27         user.setId(id);
28         user.setName(name);
29         userRepository.insert(user);
30 
31         log.info("[addUser] end!!! ");
32         //創造一個異常,看回滾情況
33         //throw new RuntimeException();
34     }
35 
36 }

6. addUserMsg方法

 1 package study.service.impl;
 2 
 3 import lombok.extern.slf4j.Slf4j;
 4 import org.springframework.stereotype.Service;
 5 import org.springframework.transaction.annotation.Transactional;
 6 import study.domain.UserMsg;
 7 import study.repository.slave.UserMsgRepository;
 8 import study.service.UserMsgService;
 9 import study.service.UserService;
10 
11 import javax.annotation.Resource;
12 
13 /**
14  * @author denny
15  * @Description
16  * @date 2018/8/27 下午5:31
17  */
18 @Slf4j
19 @Service
20 public class UserMsgServiceImpl implements UserMsgService {
21 
22     @Resource
23     private UserService userService;
24 
25     @Resource
26     private UserMsgRepository userMsgRepository;
27 
28     /**
29      * 新增帶備註的用戶:申明式分佈式事務(atomikos)
30      *
31      * @param id
32      * @param name
33      * @param msg
34      */
35     @Transactional(transactionManager = "atomikosTransactionManager", rollbackFor = Exception.class)
36     @Override
37     public void addUserMsg(int id, String name, String msg) {
38         log.info("[addUserMsg] begin!!!");
39 
40         // 1.插入用戶
41         userService.addUser(id, name);
42 
43         UserMsg userMsg = new UserMsg();
44         userMsg.setUserId(id);
45         userMsg.setMsg(msg);
46         // 2.插入用戶備註
47         userMsgRepository.insert(userMsg);
48 
49         log.info("[addUserMsg] end!!! ");
50         //創造一個異常,看回滾情況
51         //throw new RuntimeException();
52     }
53 }

注:MasterRepositoryConfig中定義了@Bean(name = "masterDataSource"),SlaveRepositoryConfig中定義了@Bean(name = "slaveDataSource"),
返回類型都是javax.sql.DataSource.必須使用@Primary註釋一個,否則spring boot啓動配置類啓動時校驗會報錯,無法定位唯一的bean,原因:飛機票

2.3  測試驗證

2.3.1 測試類

BaseTest測試基類

 1 package study;
 2 
 3 import lombok.extern.slf4j.Slf4j;
 4 import org.junit.runner.RunWith;
 5 import org.springframework.boot.test.context.SpringBootTest;
 6 import org.springframework.test.context.junit4.SpringRunner;
 7 
 8 @Slf4j
 9 @RunWith(SpringRunner.class)
10 @SpringBootTest(classes = StudyDemoApplication.class)
11 public class BaseTest {
12     
13 }

業務測試類JtaTest

 1 package study.jta;
 2 
 3 import org.junit.Test;
 4 import study.BaseTest;
 5 import study.service.UserMsgService;
 6 
 7 import javax.annotation.Resource;
 8 
 9 /**
10  * @author denny
11  * @Description 分佈式事務管理測試類
12  * @date 2018/5/11 下午6:35
13  */
14 public class JtaTest extends BaseTest {
15 
16     @Resource
17     private UserMsgService userMsgService;
18 
19     @Test
20     public void jtaTest() {
21         this.userMsgService.addUserMsg(22, "test22", "test22備註");
22     }
23 
24 }

2.3.2 結果

看控制檯日誌可見,大體流程:

  1. 初始化AtomikosDataSoureBean
  2. spring 使用JtaTransactionManager來管理分佈式事務
  3. 使用自定義的atomikos包的UserTransactionImpl和UserTransactionManager。
  4. atomikos包下的XAResourceTransaction來和mysql交互。

下一節介紹源碼剖析,深入源碼理解做了什麼。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章