由於需要重構一個老的系統(Oracle),業務側要求老系統和新系統(Mysql)並行運行半年,證明重構系統的穩定性才能上線,在這半年期間,新系統用來查詢,全文檢索,圖數據庫查詢,老系依然辦理業務,因此就存在在一個事務提交中,同時寫Mysql和Oracle,比較了一下方案,最終選擇了atomikos來做分佈式事務。先說缺點:
1、性能比原來的單純的德魯伊連接池慢。
2、卡,A庫沒提交會導致B庫也卡主,體驗非常不好。
3、德魯伊的連接池驅動jar的版本,需要和對應的數據庫特定版本的驅動保持一致,否則有些方法在德魯伊連接池都還沒實現。
4、擴展性不好,如果再需要一個BI的庫做分析,那麼又得要CDC方案從A庫同步業務數據到B庫,這個數據同步的實時性達不到業務要求。
先上代碼,把工程能跑起來先:
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>learn-jta-atomikos-SpringBoot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>learn-jta-atomikos</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- 熱部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<!-- 數據庫連接 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<!--<scope>runtime</scope>-->
<version>8.0.11</version> <!--分佈式事物的驅動,對版本有要求的,不同的數據庫,還不一樣-->
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.20</version>
</dependency>
<!-- 分佈式事務atomikos -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
<!-- tx + aop -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
<!-- 添加Log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!-- 爲log4j2添加異步支持 -->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.2</version>
</dependency>
<!-- 簡化代碼 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- 用於監控與管理 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- WEB -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 配合@ConfigurationProperties編譯生成元數據文件(IDEA編輯器的屬性提示) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- 測試 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.jms</groupId>
<artifactId>javax.jms-api</artifactId>
<version>2.0.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml
spring:
application:
name: learn-jta-atomikos
aop:
proxy-target-class: true
## jta相關參數配置
# 如果你在JTA環境中,並且仍然希望使用本地事務,你可以設置spring.jta.enabled屬性爲false以禁用JTA自動配置。
jta:
enabled:true
# 必須配置唯一的資源名
mysql:
#db1(分佈式的第一個庫)
test1:
url: jdbc:mysql://localhost:3306/db1?useUnicode=true&characterEncoding=utf-8
username: root
password: xxxx
minPoolSize: 3
maxPoolSize: 25
maxLifetime: 20000
borrowConnectionTimeout: 30
loginTimeout: 30
maintenanceInterval: 60
maxIdleTime: 60
#db2(分佈式的第二個庫)
test2:
url: jdbc:mysql://localhost:3306/db2?useUnicode=true&characterEncoding=utf-8
username: root
password: xxxxx
minPoolSize: 3
maxPoolSize: 25
maxLifetime: 20000
borrowConnectionTimeout: 30
loginTimeout: 30
maintenanceInterval: 60
maxIdleTime: 60
## Druid監控設置
datasource:
druid:
#web-stat-filter.exclusions: *.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*
stat-view-servlet.url-pattern: /druid/*
stat-view-servlet.reset-enable: true
stat-view-servlet.login-username: admin
stat-view-servlet.login-password: admin
aop-patterns: com.example.atomikos.service.*
# 開啓下劃線
mybatis:
configuration:
map-underscore-to-camel-case: true
Application.java
package com.atomikos;
import com.atomikos.config.pojo.DBConfig1;
import com.atomikos.config.pojo.DBConfig2;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
/**
* 將來這個類也是可以被Junit集成起來進行測試的
*/
@SpringBootApplication
@MapperScan("com.atomikos") //其他項目中,這個是可以不用的,可是在則個分佈式的新項目中,這個掃描Mmapper類是必須的,標記了mapper還是找不到,只好把這裏打開
@EnableConfigurationProperties(value={DBConfig1.class,DBConfig2.class}) //值對象必須加,否則後續掃描不到這個類
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
MyBatisConfig1.java
package com.atomikos.config;
import java.sql.SQLException;
import javax.sql.DataSource;
import com.atomikos.config.pojo.DBConfig1;
import com.mysql.cj.jdbc.MysqlXADataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.atomikos.jdbc.AtomikosDataSourceBean;
import org.springframework.context.annotation.Primary;
@Configuration
// basePackages 最好分開配置 如果放在同一個文件夾可能會報錯
@MapperScan(basePackages = "com.atomikos.dao.db1", sqlSessionTemplateRef = "testSqlSessionTemplate")
public class MyBatisConfig1 {
@Primary //這個primary必須加,否則spring在兩個sessionfactory的時候,不知道用哪個?
// 配置數據源
@Bean(name = "testDataSource")
public DataSource testDataSource(DBConfig1 testConfig) throws SQLException {
// 這裏直接針對mysql的分佈式驅動,進行硬編碼了
MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
mysqlXaDataSource.setUrl(testConfig.getUrl());
mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
mysqlXaDataSource.setPassword(testConfig.getPassword());
mysqlXaDataSource.setUser(testConfig.getUsername());
mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
// 將本地事務註冊到創 Atomikos全局事務
AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
xaDataSource.setXaDataSource(mysqlXaDataSource);
xaDataSource.setUniqueResourceName("testDataSource"); //硬編碼,這裏也是可以考慮落到配置文件中的
xaDataSource.setMinPoolSize(testConfig.getMinPoolSize());
xaDataSource.setMaxPoolSize(testConfig.getMaxPoolSize());
xaDataSource.setMaxLifetime(testConfig.getMaxLifetime());
xaDataSource.setBorrowConnectionTimeout(testConfig.getBorrowConnectionTimeout());
xaDataSource.setLoginTimeout(testConfig.getLoginTimeout());
xaDataSource.setMaintenanceInterval(testConfig.getMaintenanceInterval());
xaDataSource.setMaxIdleTime(testConfig.getMaxIdleTime());
xaDataSource.setTestQuery(testConfig.getTestQuery());
return xaDataSource;
}
@Primary
@Bean(name = "testSqlSessionFactory")
public SqlSessionFactory testSqlSessionFactory(@Qualifier("testDataSource") DataSource dataSource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
return bean.getObject();
}
@Primary
@Bean(name = "testSqlSessionTemplate")
public SqlSessionTemplate testSqlSessionTemplate(
@Qualifier("testSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
MyBatisConfig2.java
package com.atomikos.config;
import java.sql.SQLException;
import javax.sql.DataSource;
import com.atomikos.config.pojo.DBConfig2;
import com.mysql.cj.jdbc.MysqlXADataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.atomikos.jdbc.AtomikosDataSourceBean;
@Configuration
@MapperScan(basePackages = "com.atomikos.dao.db2", sqlSessionTemplateRef = "test2SqlSessionTemplate")
public class MyBatisConfig2 {
// 配置數據源
@Bean(name = "test2DataSource")
public DataSource testDataSource(DBConfig2 testConfig) throws SQLException {
MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
mysqlXaDataSource.setUrl(testConfig.getUrl());
mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
mysqlXaDataSource.setPassword(testConfig.getPassword());
mysqlXaDataSource.setUser(testConfig.getUsername());
mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
xaDataSource.setXaDataSource(mysqlXaDataSource);
xaDataSource.setUniqueResourceName("test2DataSource");
xaDataSource.setMinPoolSize(testConfig.getMinPoolSize());
xaDataSource.setMaxPoolSize(testConfig.getMaxPoolSize());
xaDataSource.setMaxLifetime(testConfig.getMaxLifetime());
xaDataSource.setBorrowConnectionTimeout(testConfig.getBorrowConnectionTimeout());
xaDataSource.setLoginTimeout(testConfig.getLoginTimeout());
xaDataSource.setMaintenanceInterval(testConfig.getMaintenanceInterval());
xaDataSource.setMaxIdleTime(testConfig.getMaxIdleTime());
xaDataSource.setTestQuery(testConfig.getTestQuery());
return xaDataSource;
}
@Bean(name = "test2SqlSessionFactory")
public SqlSessionFactory testSqlSessionFactory(@Qualifier("test2DataSource") DataSource dataSource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
return bean.getObject();
}
@Bean(name = "test2SqlSessionTemplate")
public SqlSessionTemplate testSqlSessionTemplate(
@Qualifier("test2SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
TransactionConfig.java
package com.atomikos.config;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.interceptor.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* 配置聲明式事務 切面攔截(本次演示中,沒啥用)
*
* @author yehaibo
*/
@Configuration
public class TransactionConfig {
private static final int TX_METHOD_TIMEOUT = 5;
private static final String AOP_POINTCUT_EXPRESSION = "execution (* com.atomikos.service.*.*(..))";
@Autowired
private PlatformTransactionManager transactionManager;
@Bean
public TransactionInterceptor txAdvice() {
NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
/* 只讀事務,不做更新操作 */
RuleBasedTransactionAttribute readOnlyTx = new RuleBasedTransactionAttribute();
readOnlyTx.setReadOnly(true);
readOnlyTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS);
/* 當前存在事務就使用當前事務,當前不存在事務就創建一個新的事務 */
RuleBasedTransactionAttribute requiredTx = new RuleBasedTransactionAttribute();
requiredTx.setRollbackRules(Collections.singletonList(new RollbackRuleAttribute(Exception.class)));
requiredTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
requiredTx.setTimeout(TX_METHOD_TIMEOUT);
Map<String, TransactionAttribute> txMap = new HashMap<>(10);
txMap.put("add*", requiredTx);
txMap.put("save*", requiredTx);
txMap.put("insert*", requiredTx);
txMap.put("update*", requiredTx);
txMap.put("delete*", requiredTx);
txMap.put("get*", readOnlyTx);
txMap.put("query*", readOnlyTx);
txMap.put("list*", readOnlyTx);
txMap.put("find*", readOnlyTx);
source.setNameMap(txMap);
return new TransactionInterceptor(transactionManager, source);
}
/**
* 切點
*
* @return
*/
@Bean
public Advisor txAdviceAdvisor() {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
return new DefaultPointcutAdvisor(pointcut, txAdvice());
}
}
DBConfig1.java
package com.atomikos.config.pojo;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
/**
* 將application.properties配置文件中配置自動封裝到實體類字段中
* @author yehaibo
*/
@ConfigurationProperties(prefix = "spring.mysql.test1") // 注意這個前綴要和application.yml文件的前綴一樣
public class DBConfig1 {
private String url;
// 比如這個url在properties中是這樣子的mysql.datasource.test1.username = root
private String username;
public String getUrl() { return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getMinPoolSize() {
return minPoolSize;
}
public void setMinPoolSize(int minPoolSize) {
this.minPoolSize = minPoolSize;
}
public int getMaxPoolSize() {
return maxPoolSize;
}
public void setMaxPoolSize(int maxPoolSize) {
this.maxPoolSize = maxPoolSize;
}
public int getMaxLifetime() {
return maxLifetime;
}
public void setMaxLifetime(int maxLifetime) {
this.maxLifetime = maxLifetime;
}
public int getBorrowConnectionTimeout() {
return borrowConnectionTimeout;
}
public void setBorrowConnectionTimeout(int borrowConnectionTimeout) {
this.borrowConnectionTimeout = borrowConnectionTimeout;
}
public int getLoginTimeout() {
return loginTimeout;
}
public void setLoginTimeout(int loginTimeout) {
this.loginTimeout = loginTimeout;
}
public int getMaintenanceInterval() {
return maintenanceInterval;
}
public void setMaintenanceInterval(int maintenanceInterval) {
this.maintenanceInterval = maintenanceInterval;
}
public int getMaxIdleTime() {
return maxIdleTime;
}
public void setMaxIdleTime(int maxIdleTime) {
this.maxIdleTime = maxIdleTime;
}
public String getTestQuery() {
return testQuery;
}
public void setTestQuery(String testQuery) {
this.testQuery = testQuery;
}
private String password;
private int minPoolSize;
private int maxPoolSize;
private int maxLifetime;
private int borrowConnectionTimeout;
private int loginTimeout;
private int maintenanceInterval;
private int maxIdleTime;
private String testQuery;
}
DBConfig2.java
package com.atomikos.config.pojo;
import org.springframework.boot.context.properties.ConfigurationProperties;
import lombok.Data;
@Data
/**
* 將application.properties配置文件中配置自動封裝到實體類字段中
* @author yehaibo
*/
@ConfigurationProperties(prefix = "spring.mysql.test2")// 注意這個前綴要和application.yml文件的前綴一樣
public class DBConfig2 {
private String url;
// 比如這個url在properties中是這樣子的mysql.datasource.test1.username = root
private String username;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getMinPoolSize() {
return minPoolSize;
}
public void setMinPoolSize(int minPoolSize) {
this.minPoolSize = minPoolSize;
}
public int getMaxPoolSize() {
return maxPoolSize;
}
public void setMaxPoolSize(int maxPoolSize) {
this.maxPoolSize = maxPoolSize;
}
public int getMaxLifetime() {
return maxLifetime;
}
public void setMaxLifetime(int maxLifetime) {
this.maxLifetime = maxLifetime;
}
public int getBorrowConnectionTimeout() {
return borrowConnectionTimeout;
}
public void setBorrowConnectionTimeout(int borrowConnectionTimeout) {
this.borrowConnectionTimeout = borrowConnectionTimeout;
}
public int getLoginTimeout() {
return loginTimeout;
}
public void setLoginTimeout(int loginTimeout) {
this.loginTimeout = loginTimeout;
}
public int getMaintenanceInterval() {
return maintenanceInterval;
}
public void setMaintenanceInterval(int maintenanceInterval) {
this.maintenanceInterval = maintenanceInterval;
}
public int getMaxIdleTime() {
return maxIdleTime;
}
public void setMaxIdleTime(int maxIdleTime) {
this.maxIdleTime = maxIdleTime;
}
public String getTestQuery() {
return testQuery;
}
public void setTestQuery(String testQuery) {
this.testQuery = testQuery;
}
private String password;
private int minPoolSize;
private int maxPoolSize;
private int maxLifetime;
private int borrowConnectionTimeout;
private int loginTimeout;
private int maintenanceInterval;
private int maxIdleTime;
private String testQuery;
}
TractionController.java
package com.atomikos.controller;
import com.atomikos.entity.BookDO;
import com.atomikos.entity.BookVo;
import com.atomikos.service.BookService;
import com.atomikos.service.impl.BookServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 頁面對接的controll類
* @author yehaibo
*/
@RestController
@RequestMapping("/books")
public class TractionController {
@Autowired
private BookService bookService;
@GetMapping
public List<BookDO> list(
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
return bookService.list(page, size);
}
@GetMapping("/{id}")
public BookDO get(@PathVariable Long id) {
return bookService.get(id);
}
@PostMapping
public BookDO save(@RequestBody BookVo book) {
return bookService.save(book, book.getUser());
}
@PutMapping
public BookDO update(@RequestBody BookVo book) {
return ((BookServiceImpl) bookService).update(book, book.getUser());
}
@DeleteMapping("/{id}")
public int delete(@PathVariable Long id) {
return ((BookServiceImpl) bookService).delete(id);
}
}
UserMapper.java
package com.atomikos.dao.db1;
import com.atomikos.entity.UserDO;
import org.apache.ibatis.annotations.*;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* mybatic有兩種寫法,這裏是mapper的寫法,不是dao的寫法
*/
@Mapper
@Repository
public interface UserMapper {
/**
* 根據主鍵查詢一條記錄
*
* @param id
* @return
*/
@Select("select id, username, password from user where id = #{id}")
UserDO get(Long id);
/**
* 分頁列表查詢
*
* @param page
* @param size
* @return
*/
@Select("select id, username, password from user limit #{page}, #{size}")
List<UserDO> list(Integer page, Integer size);
/**
* 保存
*
* @param userDO
* @return 自增主鍵
*/
@Insert("insert into user(username, password) values(#{username}, #{password})")
@Options(useGeneratedKeys = true, keyColumn = "id")
int save(UserDO userDO);
/**
* 修改一條記錄
*
* @param user
* @return
*/
@Update("update user set username = #{username}, password = #{password} where id = #{id}")
int update(UserDO user);
/**
* 刪除一條記錄
*
* @param id 主鍵
* @return
*/
@Delete("delete from user where id = #{id}")
int delete(Long id);
}
BookMapper.java
package com.atomikos.dao.db2;
import com.atomikos.entity.BookDO;
import org.apache.ibatis.annotations.*;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* @author yehaibo
* @date 2019/9/11
*/
@Mapper
@Repository
public interface BookMapper {
/**
* 分頁查詢
*
* @param page 頁碼
* @param size 每頁記錄數
* @return
*/
@Select("select id, name, article_id as articleId, user_id as userId from book limit ${page}, ${size}")
List<BookDO> list(@Param("page") Integer page, @Param("size") Integer size);
/**
* 根據主鍵查詢單條記錄
*
* @param id
* @return
*/
@Select("select id, name, article_id as articleId, user_id as userId from book where id = #{id}")
BookDO get(Long id);
/**
* 添加一條記錄
*
* @param book
* @return 自增主鍵
*/
@Insert("insert into book(name, article_id, user_id) values(#{name}, #{articleId}, #{userId})")
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
int save(BookDO book);
/**
* 修改一條記錄
*
* @param book
* @return
*/
@Update("update book set name = #{name}, article_id = #{articleId}, user_id = #{userId} where id = #{id}")
int update(BookDO book);
/**
* 刪除一條記錄
*
* @param id 主鍵
* @return
*/
@Delete("delete from book where id = #{id}")
int delete(Long id);
}
ArticleDO.java
package com.atomikos.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 文章
*
* @author yehaibo
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ArticleDO implements Serializable {
private static final long serialVersionUID = 3971756585655871603L;
private Long id;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
private String title;
private String content;
private String url;
}
BookDO.java
package com.atomikos.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 書
*
* @author yehaibo
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BookDO implements Serializable {
private static final long serialVersionUID = 3231762613546697469L;
private Long id;
//
// public BookDO(Long BookId, String Name, Long ArticleId, Long UserId) {
// this.id = BookId;
// this.name = Name;
// this.articleId = ArticleId;
// this.userId = UserId;
// }
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getArticleId() {
return articleId;
}
public void setArticleId(Long articleId) {
this.articleId = articleId;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
private String name;
private Long articleId;
private Long userId;
}
BookVo.java
package com.atomikos.entity;
import lombok.Data;
/**
* POJO的值對象
*/
@Data
public class BookVo extends BookDO {
public UserDO getUser() {
return user;
}
public void setUser(UserDO user) {
this.user = user;
}
private UserDO user;
}
UserDO.java
package com.atomikos.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 用戶
*
* @author fengxuechao
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDO implements Serializable {
private static final long serialVersionUID = 469663920369239035L;
private Long id;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
private String username;
private String password;
}
BookService.java
package com.atomikos.service;
import com.atomikos.entity.BookDO;
import com.atomikos.entity.UserDO;
import java.util.List;
/**
* 主要目的是測試分佈式事務
*
* @author yehaibo
*/
public interface BookService {
/**
* 保存
*
* @param book
* @param user
* @return
*/
BookDO save(BookDO book, UserDO user);
/**
* 單條查詢
*
* @param id
* @return
*/
BookDO get(Long id);
/**
* 分頁查詢
*
* @param page
* @param size
* @return
*/
List<BookDO> list(Integer page, Integer size);
BookDO update(BookDO book, UserDO user);
}
BookServiceImpl.java
package com.atomikos.service.impl;
import com.atomikos.dao.db1.UserMapper;
import com.atomikos.dao.db2.BookMapper;
import com.atomikos.entity.BookDO;
import com.atomikos.entity.UserDO;
import com.atomikos.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* @author yehaibo
*/
@Service("yehaibo")
public class BookServiceImpl implements BookService {
@Autowired //主要是唯一(例如類型唯一匹配上了,也是可以注入的)
private BookMapper bookMapper;
@Autowired
private UserMapper userMapper;
/**
* 保存書本和文章, 使用聲明式事務(tx+aop形式)
*
* @param book {@link BookDO}
* @param user {@link UserDO}
* @return
*/
@Override
public BookDO save(BookDO book, UserDO user) {
int userSave = userMapper.save(user);
if (userSave == 0) {
return null;
}
book.setUserId(user.getId());
int bookSave = bookMapper.save(book);
if (bookSave == 0) {
return null;
}
// throw new RuntimeException("測試分佈式事務(tx+aop形式)");
return book;
}
/**
* 單條查詢
*
* @param id
* @return
*/
@Override
public BookDO get(Long id) {
BookDO book = bookMapper.get(id);
UserDO user = userMapper.get(book.getUserId());
//?????
//BookDO(book.getId(), book.getName(), book.getArticleId(), user.getId())
return book;
}
/**
* 分頁查詢
*
* @param page
* @param size
* @return
*/
@Override
public List<BookDO> list(Integer page, Integer size) {
page = (page < 1 ? 0 : page - 1) * size;
return bookMapper.list(page, size);
}
/**
* 修改書本和文章, 使用聲明式事務(註解形式)
*
* @param book
* @param user
* @return
*/
@Transactional(rollbackFor = Exception.class)
public BookDO update(BookDO book, UserDO user) {
int bookUpdate = bookMapper.update(book);
if (bookUpdate != 1) {
return null;
}
int userUpdate = userMapper.update(user);
if (userUpdate != 1) {
return null;
}
throw new RuntimeException("測試分佈式事務(註解形式)");
// return book;
}
/**
* 刪除書本和文章
*
* @param id
* @return
*/
public int delete(Long id) {
BookDO book = bookMapper.get(id);
System.err.println(book);
if (book == null) {
throw new RuntimeException("沒有可以刪除的書本");
}
Long userId = book.getUserId();
int userDelete = userMapper.delete(userId);
if (userDelete != 1) {
return 0;
}
int bookDelete = bookMapper.delete(id);
if (bookDelete != 1) {
return 0;
}
throw new RuntimeException("測試沒有添加分佈式事務管理)");
// return 1;
}
}
BookServiceImplTest.java
package com;
import com.atomikos.entity.BookDO;
import com.atomikos.entity.UserDO;
import com.atomikos.service.BookService;
import com.atomikos.service.impl.BookServiceImpl;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
/**
* 從service層發起的測試分佈式事務:切面攔截形式, 註解式
* Junit會自己啓動springboot的框架進行測試的
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = com.atomikos.Application.class) //這個必須要加的,否則Junit無法啓動spring框架進行測試
public class BookServiceImplTest {
@Autowired
@Qualifier("yehaibo")
BookService bookService;
/**
* 測試分佈式事務(切面攔截形式)
*/
@Test
public void save() {
BookDO book = new BookDO();
book.setName("測試封面名稱 - 001");
book.setArticleId(69L);
UserDO user = new UserDO();
user.setUsername("用戶名 - 001");
user.setPassword("密碼 - 001");
BookDO bookDO = bookService.save(book, user);
System.out.println(bookDO);
}
/**
* 測試分佈式事務(註解式)
*/
@Test
public void update() {
BookDO book = new BookDO();
book.setId(10L);
book.setName("測試封面名稱 - 002");
book.setArticleId(69L);
UserDO user = new UserDO();
user.setId(18L);
user.setUsername("月用戶名 - 002");
user.setPassword("密碼 - 002");
//((BookServiceImpl)bookService).update(book, user); //已經明確指定了,不需要這樣轉換了
bookService.update(book, user);
}
/**
* 沒有事務管理
*/
@Test
public void delete() {
int delete = ((BookServiceImpl) bookService).delete(12L);
Assert.assertEquals(1, delete);
}
}
TractionControllerTest.java
package com;
import com.atomikos.dao.db1.UserMapper;
import com.atomikos.entity.BookVo;
import com.atomikos.entity.UserDO;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* 從controller層發起的mock測試分佈式事務
* 有了這種方式,就不需要postman了
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = com.atomikos.Application.class) //這個必須要加的,否則Junit無法啓動spring框架進行測試
public class TractionControllerTest {
private MockMvc mockMvc;
private ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private UserMapper userMapper;
@Autowired
private WebApplicationContext context;
@Before
public void setUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context).build();
}
/**
* 申明式
*
* @throws Exception
*/
@Test
public void save() throws Exception {
UserDO user = new UserDO();
user.setUsername("用戶名 - 002");
user.setPassword("密碼 - 002");
BookVo book = new BookVo();
book.setName("書本名稱 - 002");
book.setArticleId(69L);
book.setUser(user);
String json = objectMapper.writeValueAsString(book);
this.mockMvc.perform(
post("/books")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(json))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name", is("測試封面名稱 - 002")))
.andExpect(jsonPath("$.articleId", is(69)))
.andDo(print());
}
/**
* 註解式
*
* @throws Exception
*/
@Test
public void update() throws Exception {
UserDO user = userMapper.get(3L);
assert user != null;
user.setUsername("用戶名- 003");
user.setPassword("密碼 - 003");
BookVo book = new BookVo();
book.setId(9L);
book.setName("測試封面名稱 - 003");
book.setArticleId(69L);
book.setUser(user);
String json = objectMapper.writeValueAsString(book);
this.mockMvc.perform(
put("/books")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(json))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name", is("測試封面名稱 - 003")))
.andExpect(jsonPath("$.articleId", is(87)))
.andDo(print());
}
/**
* 沒有事務管理
*
* @throws Exception
*/
@Test
public void delete() throws Exception {
this.mockMvc.perform(
MockMvcRequestBuilders.delete("/books/4"))
.andExpect(status().isOk())
.andDo(print());
}
}