【springboot專題】 二十一 使用jta+atomikos解決多數據源分佈式事務

程序猿學社的GitHub,歡迎Star
https://github.com/ITfqyd/cxyxs
本文已記錄到github,形成對應專題。

前言

前一篇文章,我們已經實現了通過springboot+MP實現多數據源,實際上一章的代碼,如果一個方法中操作多個數據源,如果中間出現異常,可能會存在一個入庫成功,另外一個入庫失敗的問題。而沒有保證多個數據源事務的一致性。、

事務不一致問題

繼續在TestController類上加上如下代碼,兩個庫,爲了方便,我們就記錄爲test1和test2

 @ApiOperation("同時向test1和test2中插入數據,增加指定某個事務的代碼,並故意在代碼中報錯")
    @PostMapping("/saveEmp4")
    @Transactional(value = "test2TransactionManager")
    public String saveEmp4(Emp emp) {
        int insert = empMapper1.insert(emp);
          insert = empMapper2.insert(emp);
        //故意報錯
        String str= null;
        System.out.println(str.toString());   //這裏會報錯
        if(insert > 0){
            return "插入成功";
        }else{
            return "插入失敗";
        }
    };

輸入http://localhost:8080/swagger-ui.html

點擊try it out 運行。
給一個小提示,注意看清楚,配置的事務是test2TransactionManager

大家覺得test1和test2這兩個庫的emp表,會有什麼變化?
有不少社友的答案,是不是test1和test2都不插入數據。

話不多說,檢查一波數據庫的身體

  • 通過查看數據庫,可以發現test1數據源對應的庫插入成功一條“社長是個大帥哥”的記錄。 test2數據源對應的庫,表沒有變化。
  • 分析現象,可以發現,test2數據源對應的庫,沒有變化成功,就是因爲我們在方法上配置test2TransactionManager的事務,說明只有test2纔會存在事務,test1實際上是沒有事務的。
  • 通過查看@Transactional註解的源碼,alt+7(idea版本查詢類所有方法),通過仔細檢查,發現沒有一個字段能自定多個數據源。那怎麼辦?是不是就沒有別的辦法,可以解決多數據源事務的問題,別急。
  • 一般多數據源中,解決事務問題,採用jta+atomikos

jta+atomikos

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         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.2.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.cloudtech</groupId>
    <artifactId>moredatasource</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>moredatasource</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 增加thymeleaf座標  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!--簡化實體類-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- 熱部署 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

        <!--swagger2-->
        <dependency>
            <groupId>com.spring4all</groupId>
            <artifactId>spring-boot-starter-swagger</artifactId>
            <version>1.5.1.RELEASE</version>
        </dependency>

        <!--MP插件,簡化sql操作-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.18</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.6</version>
        </dependency>

        <!--swagger2-->
        <dependency>
            <groupId>com.spring4all</groupId>
            <artifactId>spring-boot-starter-swagger</artifactId>
            <version>1.5.1.RELEASE</version>
        </dependency>

        <!--jta+atomikos依賴-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jta-atomikos</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <!--解決編譯後,xml文件沒有過去的問題-->
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>include</include>
                </includes>
            </resource>
        </resources>

        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  • 跟上個版本的pom.xml相比,只是多引入一個 jta+atomikos依賴。

application.yml

server:
    port: 8888
spring:
  datasource:
    test1:
      url: jdbc:mysql://localhost:3306/pro?useUnicode=true&characterEncoding=utf8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8
      username: root
      password: root

    test2:
      url: jdbc:mysql://localhost:3306/pro1?useUnicode=true&characterEncoding=utf8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8
      username: root
      password: root



mybatis-plus:
  configuration:
    ##打印sql日誌,本地測試使用,生產環境不要使用,注意、注意、注意
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

####掃描swagger註解
swagger:
  base-package: com.cxyxs
  • 跟上個版本代碼相比,代碼有變動。

實體類

package com.cxyxs.moredatasource.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import org.omg.CORBA.IDLType;

/**
 * Description:
 * Author: 程序猿學社
 * Date:  2020/3/7 12:03
 * Modified By:
 */
@Data
public class Emp {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String name;
    private Integer age;
}

dao類

package com.cxyxs.moredatasource.test1.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cxyxs.moredatasource.entity.Emp;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * Description:
 * Author: 程序猿學社
 * Date:  2020/3/7 12:01
 * Modified By:
 */
@Repository
public interface EmpMapper1 extends  BaseMapper<Emp>{
    @Select("select * from emp")
    public List<Emp> selectList();
    /**
     * 測試mapper
     * @return
     */
    public List<Emp> getAll();
}

mapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cxyxs.moredatasource.test1.dao.EmpMapper1">
  
  <!--  根據區域名稱獲取區域代碼-->
   <select id="getAll" resultType="com.cxyxs.moredatasource.entity.Emp">
		select * from emp
  </select>
</mapper>
  • 注意:因爲test2跟test1包下的dao和mapper都是類似的,只是把test1或者EmpMapper1改爲test2和EmpMapper2就可以,這裏就不貼一些重複代碼。

讀取數據庫配置

Test1Config

package com.cxyxs.moredatasource.config;

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

/**
 * Description:V2.0版本的代碼
 * Author: 程序猿學社
 * Date:  2020/3/14 1:20
 * Modified By:
 */
@ConfigurationProperties(prefix="spring.datasource.test1")
@Data
public class Test1Config {
    private String url;
    private String username;
    private String password;
}
  • @ConfigurationProperties讀取以對應配置爲前綴的值
  • @Data使用該註解,可以不用set get
  • 發現有一個報錯提示,別慌,按提示配置一下就行,需要在application啓動類裏面增加EnableConfigurationProperties註解
    在這裏插入圖片描述

Test2Config

package com.cxyxs.moredatasource.config;

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

/**
 * Description:V2.0版本的代碼
 * Author: 程序猿學社
 * Date:  2020/3/14 1:20
 * Modified By:
 */
@ConfigurationProperties(prefix="spring.datasource.test2")
@Data
public class Test2Config {
    private String url;
    private String username;
    private String password;
}

啓動類

package com.cxyxs.moredatasource;

import com.cxyxs.moredatasource.config.Test1Config;
import com.cxyxs.moredatasource.config.Test2Config;
import com.spring4all.swagger.EnableSwagger2Doc;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan(basePackages = {"com.cxyxs"})
@EnableSwagger2Doc
@EnableConfigurationProperties(value = { Test1Config.class, Test2Config.class })
public class MoredatasourceApplication {

    public static void main(String[] args) {
        SpringApplication.run(MoredatasourceApplication.class, args);
    }
}
  • @ConfigurationProperties註解一般是需要跟@Componen配套使用的,如果沒有指定,還有一種方式,就是@EnableConfigurationPropertie表示注入。

數據庫配置

DataSourceConfigPlus1

package com.cxyxs.moredatasource.config;
 
import com.atomikos.jdbc.AtomikosDataSourceBean;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.mysql.cj.jdbc.MysqlXADataSource;
import org.apache.ibatis.plugin.Interceptor;
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 org.springframework.context.annotation.Primary;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;
import java.sql.SQLException;
 
@Configuration
@MapperScan(basePackages= {"com.cxyxs.moredatasource.test1.dao"},sqlSessionFactoryRef="test1SqlSessionFactory")
public class DataSourceConfigPlus1 {
 
	// 配置數據源
	@Bean("test1DataSource")
	public DataSource testDataSource (Test1Config 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("test1DataSource");
		return xaDataSource;
	}
 
	@Bean(name = "test1SqlSessionFactory")
	public SqlSessionFactory testSqlSessionFactory(@Qualifier("test1DataSource") DataSource dataSource,@Qualifier("test1PaginationInterceptor") PaginationInterceptor paginationInterceptor)
			throws Exception {
		//注意,這裏引入的事MP的工廠,而不是mybatis的工廠SqlSessionFactoryBean
		MybatisSqlSessionFactoryBean bean=new MybatisSqlSessionFactoryBean();
		bean.setDataSource(dataSource);
		//引入Mapper.xml文件的位置
		Resource[] resources = new PathMatchingResourcePatternResolver()
				.getResources("classpath*:/com/cxyxs/moredatasource/test1/mapper/*.xml");
		bean.setMapperLocations(resources);

		//保證MP的分頁插件可用
		Interceptor[] plugins = new Interceptor[]{paginationInterceptor};
		bean.setPlugins(plugins);
		return bean.getObject();
	}

	/**
	 * 分頁插件
	 * @return
	 */
	@Bean("test1PaginationInterceptor")
	public PaginationInterceptor paginationInterceptor(){
		return new PaginationInterceptor();
	}
 
	@Bean(name = "test1SqlSessionTemplate")
	public SqlSessionTemplate testSqlSessionTemplate(
			@Qualifier("test1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
		return new SqlSessionTemplate(sqlSessionFactory);
	}
}

DataSourceConfigPlus2

package com.cxyxs.moredatasource.config;
 
import com.atomikos.jdbc.AtomikosDataSourceBean;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.mysql.cj.jdbc.MysqlXADataSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
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 org.springframework.context.annotation.Primary;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;
import java.sql.SQLException;

@Configuration
@MapperScan(basePackages= {"com.cxyxs.moredatasource.test2.dao"},sqlSessionFactoryRef="test2SqlSessionFactory")
public class DataSourceConfigPlus2 {
 
	// 配置數據源
	@Bean("test2DataSource")
	public DataSource testDataSource (Test2Config 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");
		return xaDataSource;
	}
 
	@Bean(name = "test2SqlSessionFactory")
	public SqlSessionFactory testSqlSessionFactory(@Qualifier("test2DataSource") DataSource dataSource,@Qualifier("test2PaginationInterceptor") PaginationInterceptor paginationInterceptor)
			throws Exception {
		MybatisSqlSessionFactoryBean bean=new MybatisSqlSessionFactoryBean();
		bean.setDataSource(dataSource);
		Resource[] resources = new PathMatchingResourcePatternResolver()
				.getResources("classpath*:/com/cxyxs/moredatasource/test2/mapper/*.xml");
		bean.setMapperLocations(resources);
		Interceptor[] plugins = new Interceptor[]{paginationInterceptor};
		bean.setPlugins(plugins);
		return bean.getObject();
	}

	@Bean("test2PaginationInterceptor")
	public PaginationInterceptor paginationInterceptor(){
		return new PaginationInterceptor();
	}

	@Bean(name = "test2SqlSessionTemplate")
	public SqlSessionTemplate testSqlSessionTemplate(
			@Qualifier("test2SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
		return new SqlSessionTemplate(sqlSessionFactory);
	}
}

controller

package com.cxyxs.moredatasource.controller;

import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.cxyxs.moredatasource.entity.Emp;
import com.cxyxs.moredatasource.test1.dao.EmpMapper1;
import com.cxyxs.moredatasource.test2.dao.EmpMapper2;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;


/**
 * Description:
 * Author: 程序猿學社
 * Date:  2020/3/7 12:15
 * Modified By:
 */
@RestController
@Api("測試多數據源接口")
public class TestController {
    @Autowired
    private EmpMapper1 empMapper1;
    @Autowired
    private EmpMapper2 empMapper2;

    @ApiOperation("測試mybatis@select註解,通過test1數據庫實現")
    @GetMapping("/getKing1")
    public List getKing1(){
        List<Emp> emps = empMapper1.selectList();
        return emps;
    };


    @ApiOperation("測試mybatis@select註解,通過test2數據庫實現")
    @GetMapping("/getKing2")
    public List getKing2(){
        List<Emp> emps = empMapper2.selectList();
        return emps;
    };

    @ApiOperation("測試mybatis的mapper.xml文件調用,通過test1數據庫實現")
    @GetMapping("/getKing3")
    public List getKing3(){
        List<Emp> emps = empMapper1.getAll();
        return emps;
    };

    @ApiOperation("測試mybatis的mapper.xml文件調用,通過test2數據庫實現")
    @GetMapping("/getKing4")
    public List getKing4(){
        List<Emp> emps = empMapper2.getAll();
        return emps;
    };

    @ApiOperation("通過mp調用test1數據庫實現查詢")
    @GetMapping("/getKing5")
    public List getKing5(){
        List<Emp> emps = empMapper1.selectList(null);
        return emps;
    };

    @ApiOperation("通過mp調用test2數據庫實現查詢")
    @GetMapping("/getKing6")
    public List getKing6(){
        List<Emp> emps = empMapper2.selectList(null);
        return emps;
    };

    @ApiOperation("測試插入數據")
    @PostMapping("/saveEmp1")
    @Transactional
    public String saveEmp(Emp emp) {
        int insert = empMapper1.insert(emp);
        if(insert > 0){
            return "插入成功";
        }else{
            return "插入失敗";
        }
    };

    @ApiOperation("測試給test1插入數據,增加指定某個事務的代碼")
    @PostMapping("/saveEmp2")
    @Transactional(value = "test1TransactionManager")
    public String saveEmp2(Emp emp) {
        int insert = empMapper1.insert(emp);
        if(insert > 0){
            return "插入成功";
        }else{
            return "插入失敗";
        }
    };

    @ApiOperation("測試給test1插入數據,增加指定某個事務的代碼,並故意在代碼中報錯")
    @PostMapping("/saveEmp3")
    @Transactional(value = "test1TransactionManager")
    public String saveEmp3(Emp emp) {
        int insert = empMapper1.insert(emp);
        //故意報錯
        String str= null;
        System.out.println(str.toString());   //這裏會報錯
        if(insert > 0){
            return "插入成功";
        }else{
            return "插入失敗";
        }
    };

    @ApiOperation("同時向test1和test2中插入數據,增加指定某個事務的代碼,並故意在代碼中報錯")
    @PostMapping("/saveEmp4")
    @Transactional
    public String saveEmp4(Emp emp) {
        int insert = empMapper1.insert(emp);
        insert = empMapper2.insert(emp);
        //故意報錯
        String str= null;
        System.out.println(str.toString());   //這裏會報錯
        if(insert > 0){
            return "插入成功";
        }else{
            return "插入失敗";
        }
    };

    @ApiOperation("同時向test1和test2中插入數據,增加指定某個事務的代碼,不故意報錯")
    @PostMapping("/saveEmp5")
    @Transactional
    public String saveEmp5(Emp emp) {
        int insert = empMapper1.insert(emp);
        insert = empMapper2.insert(emp);
        if(insert > 0){
            return "插入成功";
        }else{
            return "插入失敗";
        }
    };
}
  • 只需要關注saveEmp4和saveEmp5方法

saveEmp4方法,同時操作多個數據源,故意報錯


各位社友,猜猜兩個庫的變化?

  • 直接公佈答案,就不檢查數據庫的身體,兩個庫,都插入失敗,說明事務生效。

saveEmp5方法,同時操作多個數據源,不報錯


各位社友,猜猜兩個庫的變化?

  • 兩個庫都插入生成。

淺談jta+atomikos


優點:

  • 通過jta+atomikos可以解決多數據源事務的問題,底層原理就是把多個事務,交給atomikos管理,這樣就可以幫我們解決@Transactional註解只能指定一個數據源的問題。

缺點:

  • 需要拿到數據源才能通過atomikos來管理事務,在實際開發過程中,這點幾乎很難實現,所以一般在小項目中使用。在分佈式場景中,幾乎不採用這種方式。
    源碼

覺得不錯,可以幫我點個贊,讓更多的人看到。
更多好看的內容,可以查看我的github專題,各個技術點會形成對應的專題。

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