3分鐘搞定SpringBoot+Mybatis+druid多數據源和分佈式事務

       在一些複雜的應用開發中,一個應用可能會涉及到鏈接多個數據源,所謂多數據源這裏就定義爲至少鏈接兩個及以上的數據庫了。html

       下面列舉兩種經常使用的場景:java

        一種是讀寫分離的數據源,例如一個讀庫和一個寫庫,讀庫負責各類查詢操做,寫庫負責各類添加、修改、刪除。mysql

       另外一種是多個數據源之間並無特別明顯的操做,只是程序在一個流程中可能須要同時從A數據源和B數據源中取數據或者同時往兩個數據庫插入數據等操做。git

       對於這種多數據的應用中,數據源就是一種典型的分佈式場景,所以系統在多個數據源間的數據操做必須作好事務控制。在springboot的官網中發現其支持的分佈式事務有三種Atomikos 、Bitronix、Narayana。本文涉及內容中使用的分佈式事務控制是Atomikos,感興趣的能夠查看https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-jta.htmlgithub

固然分佈式事務的做用並不只僅應用於多數據源。例如:在作數據插入的時候往一個kafka消息隊列寫消息,若是信息很重要一樣須要保證分佈式數據的一致性。spring

1、瞭解多數據源配置中的那些坑

        其實目前網上已經有許多的關於SpringBoot+Mybatis+druid+Atomikos技術棧的文章,在這裏也很感謝那些樂於分享的同行們。本文中涉及的許多的問題也是吸納了許多中外文相關技術博客文檔的優勢,算是站在巨人的肩膀作一次總結吧。拋開廢話,下面列舉一些幾點多數據源帶來的坑吧。sql

  1. 配置麻煩,尤爲是對於許多開發的新手,看了許多網上的文章,也許還配置不對,還有面對一堆的文章,可能還沒法鑑別那些文章的方法是比較可行的。
  2. 配置了多數據源後發現加入事務後並不能完成數據源的切換。
  3. 配置多數據源時發現增長了許多的配置工做量。
  4. springboot環境下mybatis應用打成jar包後沒法掃描別名。

2、如何配置一個springboot多數據源項目

    本文使用的技術棧是:SpringBoot+Mybatis+druid+Atomikos,所以使用其餘技術棧的能夠參考他人博客或者是根據本文內容改造。數據庫

重要的技術框架依賴:springboot

<!-- ali druid -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.6</version>
</dependency>
 <!-- mybatis spring -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.0</version>
</dependency>
 <!--atomikos transaction management-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>

注意:對於使用mysql jdbc 6.0的同鞋必須更新druid到最新的1.1.6,不然druid沒法支持分佈式事務。感興趣的可查看官方的release說明。restful

  1. 編寫AbstractDataSourceConfig抽象數據源配置
/**
 * 針對springboot的數據源配置
 *
 * @author yu on 2017/12/28.
 */
public abstract class AbstractDataSourceConfig {

    protected DataSource getDataSource(Environment env,String prefix,String dataSourceName){
        Properties prop = build(env,prefix);
        AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
        ds.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
        ds.setUniqueResourceName(dataSourceName);
        ds.setXaProperties(prop);
        return ds;
    }

    protected Properties build(Environment env, String prefix) {
        Properties prop = new Properties();
        prop.put("url", env.getProperty(prefix + "url"));
        prop.put("username", env.getProperty(prefix + "username"));
        prop.put("password", env.getProperty(prefix + "password"));
        prop.put("driverClassName", env.getProperty(prefix + "driver-class-name", ""));
        prop.put("initialSize", env.getProperty(prefix + "initialSize", Integer.class));
        prop.put("maxActive", env.getProperty(prefix + "maxActive", Integer.class));
        prop.put("minIdle", env.getProperty(prefix + "minIdle", Integer.class));
        prop.put("maxWait", env.getProperty(prefix + "maxWait", Integer.class));
        prop.put("poolPreparedStatements", env.getProperty(prefix + "poolPreparedStatements", Boolean.class));
        prop.put("maxPoolPreparedStatementPerConnectionSize",
                env.getProperty(prefix + "maxPoolPreparedStatementPerConnectionSize", Integer.class));
        prop.put("validationQuery", env.getProperty(prefix + "validationQuery"));
        prop.put("validationQueryTimeout", env.getProperty(prefix + "validationQueryTimeout", Integer.class));
        prop.put("testOnBorrow", env.getProperty(prefix + "testOnBorrow", Boolean.class));
        prop.put("testOnReturn", env.getProperty(prefix + "testOnReturn", Boolean.class));
        prop.put("testWhileIdle", env.getProperty(prefix + "testWhileIdle", Boolean.class));
        prop.put("timeBetweenEvictionRunsMillis", env.getProperty(prefix + "timeBetweenEvictionRunsMillis", Integer.class));
        prop.put("minEvictableIdleTimeMillis", env.getProperty(prefix + "minEvictableIdleTimeMillis", Integer.class));
        prop.put("useGlobalDataSourceStat",env.getProperty(prefix + "useGlobalDataSourceStat", Boolean.class));
        prop.put("filters", env.getProperty(prefix + "filters"));
        return prop;
    }
}

ps:AbstractDataSourceConfig對於其餘數據庫連接池的配置是能夠改動的。

2.編寫關於基於註解的動態數據源切換代碼,這部分主要是將數據庫源交給AbstractRoutingDataSource類,並由它的determineCurrentLookupKey()進行決定數據源的選擇。關於這部分的代碼,其實網上的作法基本差很少,這裏也就列舉出來了你們能夠閱讀其餘相關的博客,可是這部分的代碼是能夠單獨封裝成一個模塊的,封裝好後無論對於Springboot項目仍是SpringMVC項目將封裝的模塊導入都是能夠正常工做的。能夠參考本人目前開源的https://gitee.com/sunyurepository/ApplicationPower項目中的datasource-aspect模塊。

3.應用2中的通用封裝模塊並作寫小改動,這裏所謂的主要是你可能會像,在上面第二步中的寫的切面做用類可能沒有是用aop的註解或者是使用自定義註解的默認攔截失效,這時繼承下通用模塊中的類重寫一個AOP做用類。例如:

@Aspect
@Component
public class DbAspect extends DataSourceAspect {

    @Pointcut("execution(* com.power.learn.dao.*.*(..))")
    @Override
    protected void datasourceAspect() {
        super.datasourceAspect();
    }
}

4.編寫一個MyBatisConfig,該類的做用就是建立Mybatis多個數據源的java配置了。例如想創建兩個數據源一個叫one,另外一個叫two

@Configuration
@MapperScan(basePackages = MyBatisConfig.BASE_PACKAGE, sqlSessionTemplateRef = "sqlSessionTemplate")
public class MyBatisConfig extends AbstractDataSourceConfig {

    //mapper模式下的接口層
    static final String BASE_PACKAGE = "com.power.learn.dao";

    //對接數據庫的實體層
    static final String ALIASES_PACKAGE = "com.power.learn.model";

    static final String MAPPER_LOCATION = "classpath:com/power/learn/mapping/*.xml";


    @Primary
    @Bean(name = "dataSourceOne")
    public DataSource dataSourceOne(Environment env) {
        String prefix = "spring.datasource.druid.one.";
        return getDataSource(env,prefix,"one");
    }

    @Bean(name = "dataSourceTwo")
    public DataSource dataSourceTwo(Environment env) {
        String prefix = "spring.datasource.druid.two.";
        return getDataSource(env,prefix,"two");
    }



    @Bean("dynamicDataSource")
    public DynamicDataSource dynamicDataSource(@Qualifier("dataSourceOne")DataSource dataSourceOne,@Qualifier("dataSourceTwo")DataSource dataSourceTwo) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("one",dataSourceOne);
        targetDataSources.put("two",dataSourceTwo);

        DynamicDataSource dataSource = new DynamicDataSource();
        dataSource.setTargetDataSources(targetDataSources);
        dataSource.setDefaultTargetDataSource(dataSourceOne);
        return dataSource;
    }

    @Bean(name = "sqlSessionFactoryOne")
    public SqlSessionFactory sqlSessionFactoryOne(@Qualifier("dataSourceOne") DataSource dataSource)
        throws Exception {
        return createSqlSessionFactory(dataSource);
    }

    @Bean(name = "sqlSessionFactoryTwo")
    public SqlSessionFactory sqlSessionFactoryTwo(@Qualifier("dataSourceTwo") DataSource dataSource)
        throws Exception {
        return createSqlSessionFactory(dataSource);
    }




    @Bean(name = "sqlSessionTemplate")
    public CustomSqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactoryOne")SqlSessionFactory factoryOne,@Qualifier("sqlSessionFactoryTwo")SqlSessionFactory factoryTwo) throws Exception {
        Map<Object,SqlSessionFactory> sqlSessionFactoryMap = new HashMap<>();
        sqlSessionFactoryMap.put("one",factoryOne);
        sqlSessionFactoryMap.put("two",factoryTwo);

        CustomSqlSessionTemplate customSqlSessionTemplate = new CustomSqlSessionTemplate(factoryOne);
        customSqlSessionTemplate.setTargetSqlSessionFactorys(sqlSessionFactoryMap);
        return customSqlSessionTemplate;
    }

    /**
     * 建立數據源
     * @param dataSource
     * @return
     */
    private SqlSessionFactory createSqlSessionFactory(DataSource dataSource) throws Exception{
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setVfs(SpringBootVFS.class);
        bean.setTypeAliasesPackage(ALIASES_PACKAGE);
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_LOCATION));
        return bean.getObject();
    }
}

劃重點(考試要考):注意最後createSqlSessionFactory方法中的這一行代碼bean.setVfs(SpringBootVFS.class),對於springboot項目採用java類配置Mybatis的數據源時,mybatis自己的核心庫在springboot打包成jar後有個bug,沒法完成別名的掃描,在低版本的mybatis-spring-boot-starter中須要本身繼承Mybatis核心庫中的VFS重寫它原有的資源加載方式。在高版本的mybatis-spring-boot-starter已經幫助實現了一個叫SpringBootVFS的類。感興趣的能夠到官方項目瞭解這個bughttps://github.com/mybatis/spring-boot-starter/issues/177

5.解決分佈式事務控制下數據源沒法動態切換的問題。對於爲每個數據源建立單獨的靜態數據源而且配置固定以掃描不一樣包上的mapper接口層狀況是不會出現這種問題的,能夠很好的調用不一樣包下的mapper層,由於數據源一開就已經初始化好了,分佈式事務不會影響你調用不一樣的數據源,也不須要前面的步驟。

對於動態多數據源架構的場景,數據源都是經過aop來完成切換了,可是由於事務控制在切換以前,所以切換就被事務阻止了。曾經在解決這個問題是,很幸運的是我在google中搜索是發現了一個頗有趣的方案,而且是國內的人實現放在github上的。下面看下源碼核心。

**
 * from https://github.com/igool/spring-jta-mybatis
 */
public class CustomSqlSessionTemplate extends SqlSessionTemplate {
    
   //......省略
    @Override
    public SqlSessionFactory getSqlSessionFactory() {
        SqlSessionFactory targetSqlSessionFactory = targetSqlSessionFactorys.get(DataSourceContextHolder.getDatasourceType());
        if (targetSqlSessionFactory != null) {
            return targetSqlSessionFactory;
        } else if (defaultTargetSqlSessionFactory != null) {
            return defaultTargetSqlSessionFactory;
        } else {
            Assert.notNull(targetSqlSessionFactorys, "Property 'targetSqlSessionFactorys' or 'defaultTargetSqlSessionFactory' are required");
            Assert.notNull(defaultTargetSqlSessionFactory, "Property 'defaultTargetSqlSessionFactory' or 'targetSqlSessionFactorys' are required");
        }
        return this.sqlSessionFactory;
    }
    //......省略

}

就是重寫一個SqlSessionTemplate來改變讓SqlSessionFactory動態的獲取數據源。

targetSqlSessionFactorys.get(DataSourceContextHolder.getDatasourceType());

DataSourceContextHolder通常就是你在第二步中建立的數據源上下文操做類,這個只須要根據本身需求作改動便可。固然這個類我我的也建議像第二步同樣單獨放到一個模塊中,能夠參考本人目前開源的https://gitee.com/sunyurepository/ApplicationPower項目中的mybatis-template模塊。專門爲mybatis場景準備,可是我不建議和第二步和代碼合併在一塊兒,由於對於數據切換的切面控制代碼能夠放到非mybatis的項目中。

6.多數據源的項目配置文件配置。這裏採用yml。其配置參考以下:

#Spring boot application.yml

# spring
spring:
  #profiles : dev
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      one:
        url: jdbc:mysql://localhost:3306/project_boot?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false
        username: root
        password: root
        driver-class-name: com.mysql.cj.jdbc.Driver
        minIdle: 1
        maxActive: 20
        initialSize: 1
        timeBetweenEvictionRunsMillis: 3000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 'ZTM' FROM DUAL
        validationQueryTimeout: 10000
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        maxWait: 60000
        # 打開PSCache,而且指定每一個鏈接上PSCache的大小
        poolPreparedStatements: true
        maxPoolPreparedStatementPerConnectionSize: 20
        filters: stat,wall,log4j2
        useGlobalDataSourceStat: true
      two:
        url: jdbc:mysql://localhost:3306/springlearn?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true&useSSL=false
        username: root
        password: root
        driver-class-name: com.mysql.cj.jdbc.Driver
        minIdle: 1
        maxActive: 20
        initialSize: 1
        timeBetweenEvictionRunsMillis: 3000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 'ZTM' FROM DUAL
        validationQueryTimeout: 10000
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        maxWait: 60000
        # 打開PSCache,而且指定每一個鏈接上PSCache的大小
        poolPreparedStatements: true
        maxPoolPreparedStatementPerConnectionSize: 20
        filters: stat,wall,log4j2
        useGlobalDataSourceStat: true
  jta:
    atomikos:
      properties:
        log-base-dir: ../logs
    transaction-manager-id: txManager
server:
  port: 8080
  undertow:
     accesslog:
      enabled: true
      dir: ../logs

ps:jta就是配置讓springboot啓動分佈式事務支持。

7.編碼測試

dao層實例(對應兩個數據源,使用註解動態切換):

@TargetDataSource(DataSourceKey.ONE)
public interface StudentOneDao {

	/**
	 * 保存數據
	 * @param entity
	 * @return
     */
	int save(Student entity);

}


@TargetDataSource(DataSourceKey.TWO)
public interface StudentTwoDao {

	/**
	 * 保存數據
	 * @param entity
	 * @return
     */
	int save(Student entity);
}

service層

@Service("studentOneService")
public class StudentOneServiceImpl implements StudentService {

    /**
     * 日誌
     */
    private Logger logger = LoggerFactory.getLogger(this.getClass());

	@Resource
	private StudentOneDao studentOneDao;

	@Resource
    private StudentTwoDao studentTwoDao;
	

	@Transactional
	@Override
	public CommonResult save(Student entity) {
		CommonResult result = new CommonResult();
        try {
        	studentOneDao.save(entity);
        	studentTwoDao.save(entity);
        	int a = 10/0;
        	result.setSuccess(true);
        } catch (Exception e) {
        	logger.error("StudentService添加數據異常:",e);
        	//拋出異常讓異常restful化處理
        	throw new RuntimeException("添加數據失敗");
        }
        return result;
	}
}

ps:除0操做強行造一個異常來檢測分佈式事務是否生效,注意對於本身捕獲處理的異常狀況須要throw出去,不然事務不會生效的。能夠參考我提供的demo https://gitee.com/sunyurepository/multiple-datasource

3、如何解決這些該死的配置?

        按照上面的步驟處理後,基本就完成了一個多數據源應用的基礎架構了,可是有人會發現了,上面這麼多的配置,搞這麼多代碼,幾分鐘的時間能搞定嗎,答案基本不太可能,一不當心可能還會由於寫錯了數據源名稱又搞半天。

       所以我將介紹一種真正用幾分鐘時間來搭建一個多數據源項目的方法。幫你省掉這些重複的配置工做,輕鬆玩轉n個數據源,拋棄那些該死的配置,分分鐘建立一個demo。

第一步:下載https://gitee.com/sunyurepository/ApplicationPower項目

第二步:將Common-util、datasource-aspect、mybatis-template三個模塊安裝到你的本地maven倉庫中。對於idea的用戶只須要點3下你們都懂得,eclipse的用戶默默的抹下眼淚吧。

第三步:在application-power的resources下找到jdbc.properties鏈接一個mysql的數據庫.

第四步:在application-power的resources下找到generator.properties修改按照說明修改就行了

# @since 1.5
# 打包springboot時是否採用assembly
# 若是採用則將生成一系列的相關配置和一系列的部署腳本
generator.package.assembly=true

#@since 1.6
# 多數據源多個數據數據源用逗號隔開,不須要多數據源環境則空出來
# 對於多數據源會集成分佈式事務
generator.multiple.datasource=mysql,oracle

# @since 1.6
# jta-atomikos分佈式事務支持
generator.jta=true

主要的就是制定本身想取的數據源名稱吧,如上我一個鏈接mysql,一個鏈接oracle。其餘的根據本身的需求來改。

第五步:運行application-power的test中的

GenerateCodeTest

完成全部項目代碼的產生和輸出,而後你就能夠導入idea工具測試了。

建立完你要作的幾件事:

  1. 自動建立的項目會在dao層默認注入你配置的第一個數據源,所以須要根據本身的狀況修改,關於數據源的名稱已經自動幫你建立了一個常量類中。
  2. 給service的方法須要使用事務的方法本身加事務註解
  3. 對於非mysql數據庫你須要本身添加驅動包,建立的代碼默認添加mysql驅動包
  4. 在application.yml中修改你的數據源用戶名,密碼和鏈接的url地址,由於生成的默認是copy你鏈接數據庫生成項目時的數據庫鏈接信息。

    小結:其實建立完後整個工做就是作極少的修改,多數據源的全部配置都建立好了,連兩個和連5個數據源帶來的工做並不大。固然若是想用ApplicationPower來建立真實應用的童鞋,若是以爲模板中的一些依賴模塊不想在公司使用也是能夠稍微修改小模板來重新生成的,在使用中也但願有更好的建議被提出。

 

總結:

       本文主要只是對許多多數據源場景使用中相關優秀文章的總結。我我的僅僅是將這些總結的東西經過封裝和我我的開源放在碼雲上的ApplicationPower腳手架將SpringBoot+Mybatis+druid+Atomikos的多數據源和分佈式事務架構的配置經過自動化來快速輸出。

申明:轉載本博客內容請註明原地址http://www.javashuo.com/article/p-ucjreklo-dx.html

參考博客:

http://blog.csdn.net/a510835147/article/details/75675311等

相關文章
相關標籤/搜索