Spring Boot2中整合atomikos來實現不同類型數據庫的分佈式事務一致性

由於需要重構一個老的系統(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());
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章