Spring Boot 多數(shù)據(jù)源醉冤,整合 Atomikos 實現(xiàn)分布式事務(wù)

前言

由于最近的項目需要整合兩個數(shù)據(jù)庫秩霍,有些業(yè)務(wù)邏輯也涉及到兩個數(shù)據(jù)庫同時插入、更新的操作蚁阳;所以就涉及到跨數(shù)據(jù)庫的數(shù)據(jù)一致性問題铃绒。于是基于 Spring Boot 整合了 Atomikos 的一個項目 demo。
項目源碼地址:https://github.com/WongMinHo/spring-boot-api-starter

介紹

  • 分布式事務(wù):

分布式事務(wù)螺捐,可以理解為:由于分布式而引起的事務(wù)不一致的問題颠悬。隨著項目做大,模塊拆分定血,數(shù)據(jù)庫拆分赔癌。一次包含增刪改操作數(shù)據(jù)庫涉及到了更新兩個不同物理節(jié)點的數(shù)據(jù)庫,這樣的數(shù)據(jù)庫事務(wù)只能保證自己處理的部分的事務(wù)澜沟,但是整個的事務(wù)就不能保證一致性灾票。

  • JTA:

JTA(java Transaction API)是 JavaEE 13 個開發(fā)規(guī)范之一纷铣,java 事務(wù)API患久,允許應(yīng)用程序執(zhí)行分布式事務(wù)處理——在兩個或多個網(wǎng)絡(luò)計算機資源上訪問并且更新數(shù)據(jù)。JDBC 驅(qū)動程序的 JTA 支持極大地增強了數(shù)據(jù)訪問能力仲吏。事務(wù)就是保證數(shù)據(jù)的有效性濒析,數(shù)據(jù)的一致性正什。

  • Atomikos:

Atomikos 是一個為 Java 平臺提供增值服務(wù)的并且開源類事務(wù)管理器,主要用于處理跨數(shù)據(jù)庫事務(wù)号杏;在 Spring Boot 的文檔也推薦更多人使用 Atomikos婴氮。

實現(xiàn)案例

場景:兩個數(shù)據(jù)庫,分別是minhow_first馒索、minhow_second莹妒;包含 mh_user 用戶表、 mh_customer 客戶表绰上。

項目結(jié)構(gòu):


directory-structure
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.9.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.minhow</groupId>
    <artifactId>spring-boot-api-starter</artifactId>
    <version>1.0</version>
    <name>spring-boot-api-starter</name>
    <description>Spring Boot Seed Project</description>

    <properties>
        <java.version>1.8</java.version>
        <mybatis-plus.version>3.2.0</mybatis-plus.version>
        <mybatis-plus-generator.version>3.2.0</mybatis-plus-generator.version>
        <guava.version>27.1-jre</guava.version>
        <common-lang3.version>3.9</common-lang3.version>
        <fastjson.version>1.2.60</fastjson.version>
        <druid.version>1.1.20</druid.version>
        <jjwt.version>0.9.1</jjwt.version>
        <velocity-engine.version>2.0</velocity-engine.version>
        <mysql-connector.version>8.0.11</mysql-connector.version>
        <lombok.version>1.18.10</lombok.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- jta-atomikos 分布式事務(wù)管理 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jta-atomikos</artifactId>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <optional>true</optional>
        </dependency>
        <!-- mysql connector-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql-connector.version}</version>
        </dependency>

        <!-- mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>${mybatis-plus-generator.version}</version>
        </dependency>
        <!-- 模板引擎 -->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>${velocity-engine.version}</version>
        </dependency>

        <!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- Alibaba -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <!-- jjwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>${jjwt.version}</version>
        </dependency>

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${guava.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>${commons-lang3.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
application.yml 數(shù)據(jù)源配置:
# 本地環(huán)境配置文件
spring:
  datasource:
    druid:
      first:  #數(shù)據(jù)源1
        driver-class-name: com.mysql.cj.jdbc.Driver
        type: com.alibaba.druid.pool.xa.DruidXADataSource
        url: jdbc:mysql://localhost:3306/minhow_first?useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai&autoReconnect=true&characterEncoding=utf8
        username: root
        password: root
        #初始化時建立物理連接的個數(shù)
        initial-size: 5
        #池中最大連接數(shù)
        max-active: 20
        #最小空閑連接
        min-idle: 1
        #獲取連接時最大等待時間,單位毫秒
        max-wait: 60000
        #有兩個含義:
        #1) Destroy線程會檢測連接的間隔時間渠驼,如果連接空閑時間大于等于minEvictableIdleTimeMillis則關(guān)閉物理連接蜈块。
        #2) testWhileIdle的判斷依據(jù),詳細看testWhileIdle屬性的說明
        time-between-eviction-runs-millis: 60000
        #連接保持空閑而不被驅(qū)逐的最小時間迷扇,單位是毫秒
        min-evictable-idle-time-millis: 300000
        #使用該SQL語句檢查鏈接是否可用百揭。如果validationQuery=null,testOnBorrow蜓席、testOnReturn器一、testWhileIdle都不會起作用。
        validationQuery: SELECT 1 FROM DUAL
        #建議配置為true厨内,不影響性能祈秕,并且保證安全性渺贤。申請連接的時候檢測,如果空閑時間大于timeBetweenEvictionRunsMillis请毛,執(zhí)行validationQuery檢測連接是否有效志鞍。
        test-while-idle: true
        #申請連接時執(zhí)行validationQuery檢測連接是否有效,做了這個配置會降低性能方仿。
        test-on-borrow: false
        #歸還連接時執(zhí)行validationQuery檢測連接是否有效固棚,做了這個配置會降低性能。
        test-on-return: false
        # 配置監(jiān)控統(tǒng)計攔截的filters仙蚜,去掉后監(jiān)控界面sql無法統(tǒng)計此洲,'wall'用于防火墻
        filters: stat,wall,slf4j
        # 通過connectProperties屬性來打開mergeSql功能;慢SQL記錄
        connect-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
      second: #數(shù)據(jù)源2
        driver-class-name: com.mysql.cj.jdbc.Driver
        type: com.alibaba.druid.pool.xa.DruidXADataSource
        url: jdbc:mysql://localhost:3306/minhow_second?useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai&autoReconnect=true&characterEncoding=utf8
        username: root
        password: root
        #初始化時建立物理連接的個數(shù)
        initial-size: 5
        #池中最大連接數(shù)
        max-active: 20
        #最小空閑連接
        min-idle: 1
        #獲取連接時最大等待時間委粉,單位毫秒
        max-wait: 60000
        #有兩個含義:
        #1) Destroy線程會檢測連接的間隔時間黍翎,如果連接空閑時間大于等于minEvictableIdleTimeMillis則關(guān)閉物理連接。
        #2) testWhileIdle的判斷依據(jù)艳丛,詳細看testWhileIdle屬性的說明
        time-between-eviction-runs-millis: 60000
        #連接保持空閑而不被驅(qū)逐的最小時間匣掸,單位是毫秒
        min-evictable-idle-time-millis: 300000
        #使用該SQL語句檢查鏈接是否可用。如果validationQuery=null氮双,testOnBorrow碰酝、testOnReturn、testWhileIdle都不會起作用戴差。
        validationQuery: SELECT 1 FROM DUAL
        #建議配置為true送爸,不影響性能,并且保證安全性暖释。申請連接的時候檢測袭厂,如果空閑時間大于timeBetweenEvictionRunsMillis,執(zhí)行validationQuery檢測連接是否有效球匕。
        test-while-idle: true
        #申請連接時執(zhí)行validationQuery檢測連接是否有效纹磺,做了這個配置會降低性能。
        test-on-borrow: false
        #歸還連接時執(zhí)行validationQuery檢測連接是否有效亮曹,做了這個配置會降低性能橄杨。
        test-on-return: false
        # 配置監(jiān)控統(tǒng)計攔截的filters,去掉后監(jiān)控界面sql無法統(tǒng)計照卦,'wall'用于防火墻
        filters: stat,wall,slf4j
        # 通過connectProperties屬性來打開mergeSql功能式矫;慢SQL記錄
        connect-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
創(chuàng)建兩個數(shù)據(jù)庫和數(shù)據(jù)表sql:
#創(chuàng)建第一個數(shù)據(jù)庫和數(shù)據(jù)表
CREATE DATABASE minhow_first;
-- ----------------------------
-- Table structure for mh_user
-- ----------------------------
USE minhow_first;
DROP TABLE IF EXISTS `mh_user`;
CREATE TABLE `mh_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(191) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '姓名',
  `password` varchar(191) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '密碼',
  `customer_num` int(11) DEFAULT '0' COMMENT '客戶數(shù)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

-- ----------------------------
-- Records of mh_user
-- ----------------------------
INSERT INTO `mh_user` VALUES (1, 'minhow', NULL, 0);

#創(chuàng)建第二個數(shù)據(jù)庫和數(shù)據(jù)表
CREATE DATABASE minhow_second;
-- ----------------------------
-- Table structure for mh_customer
-- ----------------------------
USE minhow_second;
DROP TABLE IF EXISTS `mh_customer`;
CREATE TABLE `mh_customer` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL COMMENT '用戶id',
  `name` varchar(191) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '姓名',
  `phone` varchar(11) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '手機號',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
第一個數(shù)據(jù)源FirstDataSourceProperties配置:
package com.minhow.springbootapistarter.config.datasource;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @author MinHow
 * @date 2018/3/4 7:13 下午
 */
@Data
@Component
@ConfigurationProperties(prefix = "spring.datasource.druid.first")
public class FirstDataSourceProperties {
    private String url;

    private String username;

    private String password;

    private String driverClassName;

    private String type;

    private Integer initialSize;

    private Integer minIdle;

    private Integer maxActive;

    private Integer maxWait;

    private Integer timeBetweenEvictionRunsMillis;

    private Integer minEvictableIdleTimeMillis;

    private String validationQuery;

    private Boolean testWhileIdle;

    private String testOnBorrow;

    private String testOnReturn;

    private String poolPreparedStatements;

    private String filters;

    private String connectionProperties;

    private String initConnectionSqls;
}
第二個數(shù)據(jù)源SecondDataSourceProperties配置:
package com.minhow.springbootapistarter.config.datasource;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @author MinHow
 * @date 2018/3/4 7:13 下午
 */
@Data
@Component
@ConfigurationProperties(prefix = "spring.datasource.druid.second")
public class SecondDataSourceProperties {
    private String url;

    private String username;

    private String password;

    private String driverClassName;

    private String type;

    private Integer initialSize;

    private Integer minIdle;

    private Integer maxActive;

    private Integer maxWait;

    private Integer timeBetweenEvictionRunsMillis;

    private Integer minEvictableIdleTimeMillis;

    private String validationQuery;

    private Boolean testWhileIdle;

    private String testOnBorrow;

    private String testOnReturn;

    private String poolPreparedStatements;

    private String filters;

    private String connectionProperties;

    private String initConnectionSqls;
}
第一個數(shù)據(jù)源FirstDataSourceConfiguration配置:

注意:如果使用Druid的分布式驅(qū)動,暫不支持MySql8.0+

package com.minhow.springbootapistarter.config.datasource;

import com.alibaba.druid.pool.xa.DruidXADataSource;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.minhow.springbootapistarter.common.constant.DBConstants;
import com.mysql.cj.jdbc.MysqlXADataSource;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

/**
 * @author MinHow
 * @date 2018/3/4  7:20 下午
 */
@Configuration
@MapperScan(basePackages = DBConstants.FIRST_MAPPER, sqlSessionFactoryRef = DBConstants.FIRST_SQL_SESSION_FACTORY)
@Slf4j
public class FirstDataSourceConfiguration {
    @Autowired
    private FirstDataSourceProperties firstDataSourceProperties;

    /**
     * 配置第一個數(shù)據(jù)源
     * @return
     */
    @Primary
    @Bean(DBConstants.FIRST_DATA_SOURCE)
    public DataSource firstDataSource() {
//        使用Druid的分布式驅(qū)動役耕,暫時發(fā)現(xiàn)不支持MySql8以上的版本
//        DruidXADataSource druidXADataSource = new DruidXADataSource();
//        BeanUtils.copyProperties(firstDataSourceProperties, druidXADataSource);

        //使用mysql的分布式驅(qū)動采转,支持MySql5.*、MySql8.* 以上版本
        MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
        mysqlXaDataSource.setUrl(firstDataSourceProperties.getUrl());
        mysqlXaDataSource.setPassword(firstDataSourceProperties.getPassword());
        mysqlXaDataSource.setUser(firstDataSourceProperties.getUsername());

        AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
        xaDataSource.setXaDataSource(mysqlXaDataSource);
        xaDataSource.setUniqueResourceName(DBConstants.FIRST_DATA_SOURCE);
        xaDataSource.setPoolSize(firstDataSourceProperties.getInitialSize());
        xaDataSource.setMinPoolSize(firstDataSourceProperties.getMinIdle());
        xaDataSource.setMaxPoolSize(firstDataSourceProperties.getMaxActive());
        xaDataSource.setMaxIdleTime(firstDataSourceProperties.getMinIdle());
        xaDataSource.setMaxLifetime(firstDataSourceProperties.getMinEvictableIdleTimeMillis());
        xaDataSource.setConcurrentConnectionValidation(firstDataSourceProperties.getTestWhileIdle());
        xaDataSource.setTestQuery(firstDataSourceProperties.getValidationQuery());

        return xaDataSource;
    }

    /**
     * 創(chuàng)建第一個SqlSessionFactory
     * @param firstDataSource
     * @return
     * @throws Exception
     */
    @Primary
    @Bean(DBConstants.FIRST_SQL_SESSION_FACTORY)
    public SqlSessionFactory firstSqlSessionFactory(@Qualifier(DBConstants.FIRST_DATA_SOURCE) DataSource firstDataSource)
            throws Exception {
        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
        bean.setDataSource(firstDataSource);
        //設(shè)置mapper位置
        bean.setTypeAliasesPackage(DBConstants.FIRST_MAPPER);
        //設(shè)置mapper.xml文件的路徑
        bean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources(DBConstants.FIRST_MAPPER_XML));

        return bean.getObject();
    }
}
第二個數(shù)據(jù)源SecondDataSourceConfiguration配置:

注意:如果使用Druid的分布式驅(qū)動瞬痘,暫不支持MySql8.0+

package com.minhow.springbootapistarter.config.datasource;

import com.alibaba.druid.pool.xa.DruidXADataSource;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.minhow.springbootapistarter.common.constant.DBConstants;
import com.mysql.cj.jdbc.MysqlXADataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

/**
 * @author MinHow
 * @date 2018/3/4  7:20 下午
 */
@Configuration
@MapperScan(basePackages = DBConstants.SECOND_MAPPER, sqlSessionFactoryRef = DBConstants.SECOND_SQL_SESSION_FACTORY)
public class SecondDataSourceConfiguration {
    @Autowired
    private SecondDataSourceProperties secondDataSourceProperties;

    /**
     * 配置第二個數(shù)據(jù)源
     * @return
     */
    @Bean(DBConstants.SECOND_DATA_SOURCE)
    public DataSource secondDataSource() {
//        使用Druid的分布式驅(qū)動故慈,暫時發(fā)現(xiàn)不支持mysql8以上的版本
//        DruidXADataSource druidXADataSource = new DruidXADataSource();
//        BeanUtils.copyProperties(secondDataSourceProperties, druidXADataSource);

        //使用mysql的分布式驅(qū)動板熊,支持mysql5.*、mysql8.* 以上版本
        MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
        mysqlXaDataSource.setUrl(secondDataSourceProperties.getUrl());
        mysqlXaDataSource.setPassword(secondDataSourceProperties.getPassword());
        mysqlXaDataSource.setUser(secondDataSourceProperties.getUsername());

        AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
        xaDataSource.setXaDataSource(mysqlXaDataSource);
        xaDataSource.setUniqueResourceName(DBConstants.SECOND_DATA_SOURCE);
        xaDataSource.setPoolSize(secondDataSourceProperties.getInitialSize());
        xaDataSource.setMinPoolSize(secondDataSourceProperties.getMinIdle());
        xaDataSource.setMaxPoolSize(secondDataSourceProperties.getMaxActive());
        xaDataSource.setMaxIdleTime(secondDataSourceProperties.getMinIdle());
        xaDataSource.setMaxLifetime(secondDataSourceProperties.getMinEvictableIdleTimeMillis());
        xaDataSource.setConcurrentConnectionValidation(secondDataSourceProperties.getTestWhileIdle());
        xaDataSource.setTestQuery(secondDataSourceProperties.getValidationQuery());

        return xaDataSource;
    }

    /**
     * 創(chuàng)建第二個SqlSessionFactory
     * @param secondDataSource
     * @return
     * @throws Exception
     */
    @Bean(DBConstants.SECOND_SQL_SESSION_FACTORY)
    public SqlSessionFactory secondSqlSessionFactory(@Qualifier(DBConstants.SECOND_DATA_SOURCE) DataSource secondDataSource)
            throws Exception {
        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
        bean.setDataSource(secondDataSource);
        //設(shè)置mapper位置
        bean.setTypeAliasesPackage(DBConstants.SECOND_MAPPER);
        //設(shè)置mapper.xml文件的路徑
        bean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources(DBConstants.SECOND_MAPPER_XML));

        return bean.getObject();
    }
}
Atomikos配置:
package com.minhow.springbootapistarter.config.datasource;

import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.jta.JtaTransactionManager;

import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;

/**
 * 事務(wù)管理
 * @author jacker
 * @date 2019/8/13 3:41 PM
 */
@Configuration
@EnableTransactionManagement
public class TransactionManagerConfig {
    @Bean(name = "userTransaction")
    public UserTransaction userTransaction() throws Throwable {
        UserTransactionImp userTransactionImp = new UserTransactionImp();
        userTransactionImp.setTransactionTimeout(10000);
        return userTransactionImp;
    }

    @Bean(name = "atomikosTransactionManager")
    public TransactionManager atomikosTransactionManager() throws Throwable {
        UserTransactionManager userTransactionManager = new UserTransactionManager();
        userTransactionManager.setForceShutdown(false);
        return userTransactionManager;
    }

    @Bean(name = "transactionManager")
    @DependsOn({"userTransaction", "atomikosTransactionManager"})
    public PlatformTransactionManager transactionManager() throws Throwable {
        return new JtaTransactionManager(userTransaction(), atomikosTransactionManager());
    }
}

通過 @EnableTransactionManagement 來啟用事務(wù)管理惯悠,該注解會自動查找滿足條件的PlatformTransactionManager邻邮;更詳細的配置方法可以參見 Atomikos Spring Integration
還有 Dao 和 Mapper 的代碼就不貼了克婶,詳情請看項目源碼筒严。
至此為止,配置就完成了情萤,之后只需要在事務(wù)控制的地方加上 @Transactional 注解即可鸭蛙。

案例:

業(yè)務(wù)流程:在 mh_customer 客戶表新增記錄,mh_user 用戶表客戶數(shù)增加1筋岛,代碼如下:

package com.minhow.springbootapistarter.service.second.impl;

import com.minhow.springbootapistarter.common.enums.ResultEnum;
import com.minhow.springbootapistarter.common.exception.BusinessException;
import com.minhow.springbootapistarter.common.response.Result;
import com.minhow.springbootapistarter.pojo.dto.StoreCustomerDTO;
import com.minhow.springbootapistarter.pojo.entity.first.User;
import com.minhow.springbootapistarter.pojo.entity.second.Customer;
import com.minhow.springbootapistarter.dao.second.mapper.CustomerMapper;
import com.minhow.springbootapistarter.service.first.UserService;
import com.minhow.springbootapistarter.service.second.CustomerService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * <p>
 *  服務(wù)實現(xiàn)類
 * </p>
 *
 * @author MinHow
 * @since 2019-10-05
 */
@Service
public class CustomerServiceImpl extends ServiceImpl<CustomerMapper, Customer> implements CustomerService {
    @Autowired
    private UserService userService;
    /**
     * 新增客戶 - 演示多數(shù)據(jù)源分布式事務(wù)
     * @param storeCustomerDTO
     * @return
     */
    @Override
    @Transactional(rollbackFor = BusinessException.class)
    public Result store(StoreCustomerDTO storeCustomerDTO) {
        User user = userService.lambdaQuery()
                .select(User::getId, User::getCustomerNum)
                .eq(User::getId, storeCustomerDTO.getUserId())
                .one();

        if (user == null) {
            return Result.fail(4001, "用戶不存在");
        }

        Customer customer = new Customer();
        customer.setName(storeCustomerDTO.getCustomerName())
                .setPhone(storeCustomerDTO.getCustomerPhone())
                .setUserId(storeCustomerDTO.getUserId());
        //添加客戶
        boolean customerStatus = this.save(customer);

        //更新用戶客戶數(shù)
        boolean userStatus = userService.lambdaUpdate()
                .set(User::getCustomerNum, user.getCustomerNum() + 1)
                .eq(User::getId, storeCustomerDTO.getUserId())
                .update();
        //不符合條件娶视,兩個數(shù)據(jù)庫表數(shù)據(jù)回滾
        if (! customerStatus || ! userStatus) {
            throw new BusinessException(ResultEnum.BUSINESS_ERROR);
        }

        return Result.ok();
    }
}

通過修改不同條件,測試事務(wù)回滾和不回滾的結(jié)果睁宰,就能測試分布式事務(wù)是否得到支持肪获。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市柒傻,隨后出現(xiàn)的幾起案子孝赫,更是在濱河造成了極大的恐慌,老刑警劉巖红符,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件青柄,死亡現(xiàn)場離奇詭異,居然都是意外死亡预侯,警方通過查閱死者的電腦和手機致开,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來萎馅,“玉大人双戳,你說我怎么就攤上這事⌒?樱” “怎么了拣技?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長耍目。 經(jīng)常有香客問我,道長徐绑,這世上最難降的妖魔是什么邪驮? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮傲茄,結(jié)果婚禮上毅访,老公的妹妹穿的比我還像新娘沮榜。我一直安慰自己,他們只是感情好喻粹,可當(dāng)我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布蟆融。 她就那樣靜靜地躺著,像睡著了一般守呜。 火紅的嫁衣襯著肌膚如雪型酥。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天查乒,我揣著相機與錄音弥喉,去河邊找鬼。 笑死玛迄,一個胖子當(dāng)著我的面吹牛由境,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蓖议,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼虏杰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了勒虾?” 一聲冷哼從身側(cè)響起纺阔,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎从撼,沒想到半個月后州弟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡低零,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年婆翔,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掏婶。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡啃奴,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出雄妥,到底是詐尸還是另有隱情最蕾,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布老厌,位于F島的核電站瘟则,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏枝秤。R本人自食惡果不足惜醋拧,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧丹壕,春花似錦庆械、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至琉用,卻和暖如春堕绩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背辕羽。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工逛尚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人刁愿。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓绰寞,卻偏偏與公主長得像,于是被迫代替她去往敵國和親铣口。 傳聞我的和親對象是個殘疾皇子滤钱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容