关于多数据源的那些事儿(萌新向)

关于多数据源的那些事儿(萌新向)

        在日常的JAVA后端开发中多数据源的应用场景并不少见,但对于刚刚接触springboot或是刚刚接触工程化开发的萌新来说却仿佛是一座不可逾越的高山,因为新手常常会局限于某些“固定的”项目配置,不知道如何配置?从哪里开始配置?以及什么能改什么不能改。这种现象在用惯了springboot便捷开发的老手中也很常见,众所周知,相比于spring的springboot简化了很多工程前置配置,虽然增加了工作效率却也使得开发人员失去了了解基础配置的机会。综上,本文主要讲解如何在springboot环境中,以一种最简单的、即起即用的、不依赖中间件和数据库切片的方式配置单一项目的多数据源。限于笔者能力有限,经验尚浅,若有描述不当之处,敬请批评指正。同样地本文在多数技术概念上皆是浅尝辄止,未能深入之处还请见谅。

        

1.什么是多数据源?我们为什么要配置?多数据源又有哪几种实现方式?

        在大多数以SSM为开发模型的web项目中,通常都是单数据源的,也就是一个项目只连接一个数据库(也可能是n个一模一样的数据库,其实这种情况也算是多数据源了),尤其是近来流行的“微服务”理念使得后端开发过程不断解耦,大多是时候我们都会为了某块功能指定某个数据库,从而实现程序上的“高内聚,低耦合”和数据上的“解耦”。但这种现状并不能解决所有的需求,尤其像是A、B数据库数据定期转移一类的项目,就必然要使用多数据源解决问题了。

        多数据源的实现,往大了说是架构层的事,可以做分布式数据库,比如使用Mycat这种数据库的分库分表中间件,Mycat的本质是一个开源的MySQL集群数据库,因为这个中间件的主要作用是将数据库切片从而提高并发效率,故可以用来实现多数据源,不过像数据转移这种小项目就有点大材小用了。同理还不得不提的是Sharding-JDBC,也是架构级的数据库分布中间件,但却拥有其他中间件无法比拟的性能优势,官方声称单库查询QPS为原生JDBC的99.8%;双库查询QPS比单库增加94%。由于笔者也是个萌新,正所谓初生牛犊不怕虎,可怕的是我连Sharding-JDBC是一只大老虎都不知道,所以在这个项目的初期构思阶段还试图使用Sharding-JDBC来实现需求,还好架构老哥及时制止了我,据称Sharding-JDBC的配置难度极大,并不是萌新能搞得定的。

        说了这么多,其实都是我在构思阶段走的弯路,以上的实现方式很强大,却不适合这种即起即用、高度灵活的小项目,故此我来介绍最简单的一种实现方式,无需依赖任何的外部中间件,也不需要对数据库进行切片,而是直接手动配置bean和datasource以及SessionFactory。网上类似的帖子很多,我会在适当的地方贴两个链接,方便大家理解。

2.多数据源的简单原理及运行环境

        在此之前,如果你不知道什么是datasource和SessionFactory,可以先看一下https://blog.csdn.net/u012881904/article/details/77449710里的图,只看图就可以理解springboot是怎么把数据源抽象成实体类以供我们调用的了。当然如果你觉得这个链接有点复杂没关系,因为你完全不必理解数据是从数据库的字段值变成了实体类的属性的这个过程,你只需知道有这么个过程,而且在单数据源时springboot自动帮我们做了,就可以了。现在我们要做的就是关闭springboot关于以上步骤的自动配置,然后手动配,让项目初始化的时候加载多个datasource、生成多个SessionFactory然后映射成多个Bean就好了。

        再说说运行环境吧,我查阅的网上的资料,都没有比我的开发环境版本更高,如果你是用的是当下流行的架构,那么这篇文章会给你很大帮助。

        以下是环境:springboot 2.1 ; mybatisplus 3.0 ; spring-jdbc 5.1 ; gradle 5.3.1。

3.多数据源的简单实现,把大象放进冰箱也就只需三步

        (1)找到数据库配置文件,我用的gradle,所以是application.yml,更改里面数据库到配置,写多个数据库的链接,数据库的名字建议小写,多少个无所谓。我的jdbc版本很高所以驱动不是com.mysql.jdbc.Driver。

spring:
          a:
	      driver-class-name: com.mysql.cj.jdbc.Driver
	      jdbc-url: xxxx
	      username: xxxx
	      password: xxxx
	  b:
	      driver-class-name: com.mysql.cj.jdbc.Driver
	      jdbc-url: xxxx
	      username: xxxx
	      password: xxxx
	  c:
	      driver-class-name: com.mysql.cj.jdbc.Driver
	      jdbc-url: xxxx
	      username: xxxx
	      password: xxxx

        这里不得不吐槽一下application.yml非人类的格式要求,注意缩进量,特别地,如果你用的也是gradle和springboot,这个配置文件下有个datasource属性,正常单数据源下数据库是写在datasource下的,这里不建议再把多数据源放在datasource下,直接写在外面,也不建议在最外面。我上面的代码,直接写在了spring根层级下。原因是springboot对于datasource是有特殊识别的你要分主库和辅库、完全没必要;如果写在和spring同级,有可能加载不到这个数据库配置,原因未知。

        最后再说一下,不管你是什么类型的配置文件,在这一步你要做的就是定义一个数据库,并给它起个名字(比如a),方便后面我们找到它。

     (2)找到Application启动类,关掉springboot服务启动时对于数据源配置的自动导入

package com.xxx.xxx;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;


@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
//@SpringBootApplication
@ComponentScan("com.xxx")
public class ApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(ApiApplication.class,args);
    }

        可以看到我直接注释掉了原来的@SpringBootApplication自动加载数据源注解,用@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})代替,这是核心的一步,这个注解意味着你在启动项目时会关掉springboot专门为简化开发效率而进行的自动注入数据源,产生Bean映射。

        (3)最终步骤,为每一个数据源建立各自的config引导

        让我们从头捋一捋,看看你对这该死的项目做了什么,你在第二步关掉了一个关键的自动配置,相当于拿走了内燃机的输油管,所以现在这车是启动不了的,会报错;你在第一步配置了多个数据库,相当于加了几个油箱;那么思路就很清晰了,你只需要在第三步手动给这几个油箱配上各自的输油管插到内燃机上就可以了。

        每人的架构大同小异,但你的controll层至少要有三个文件,第一个文件是刚刚第二步用到的Application启动类,不管它叫什么这个启动类是唯一的、而且必然存在的;第二个文件是一个放置各种实现业务的controller类的包,当然你也可以把controller类直接这么放在这层;第三个文件是至关重要的名为config的包,我们会在里面为每一个数据源配置各自的"输油管"。文件结构如下:

--com.xxx
  |--Application
  |--controller包
  |--config包
     |--aConfig.java
     |--bConfig.java
     |--cConfig.java

        然后在config包为每一个库都创建一个类,这些类的配置基本相同,我会在代码的注释中标明差异,以下是主库a(必有且仅有一个是主库)的aConfig.java

package com.xxx.config;

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.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;

@Configuration
//下面{"com.xxx.mapper.a"}里面填的是你的mapper接口的包名,也就是说一个数据源的mapper都要在一个包里
@MapperScan(basePackages = {"com.xxx.mapper.a"}, sqlSessionFactoryRef = "sqlSessionFactory")
public class aConfig {
  
    @Primary//只有主库a才写这个@Primary
    @Bean
    //下面""中的值就是第一步填的数据库名,我写在了spring下
    @ConfigurationProperties(prefix = "spring.a")
    public DataSource dataSourceSentence() {
        return DataSourceBuilder.create().build();
    }

    @Primary//只有主库a才写这个@Primary
    @Bean
    @DependsOn("dataSourceSentence")
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceSentence());
       //下面这句可有可无,如果你有Sql的xml文件就必须配进去,""里面是你自己的xml路径。
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("/*.xml"));
        return sqlSessionFactoryBean.getObject();
    }

    @Primary//只有主库a才写这个@Primary
    @Bean
    @DependsOn("sqlSessionFactory")
    public SqlSessionTemplate sqlSessionTemplate() throws Exception {
        SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory());
        return sqlSessionTemplate;
    }
}

        到这里就完成全部配置了,其他库只要删掉@Primary注解,改一下类名、方法名不要冲突就可以了,注意每个数据源都配一次,且要把每个数据源的实体类、mapper等等分开不同的包。

        再次感谢您的浏览,开源万岁!!!∠(`ω´*)۶


2019-06-28Agostino

{{blog.title}}

创建于 {{blog.createTimeStr}}   created  by  {{blog.author}} {{tag}}
最后修改于 {{blog.timelineStr}}
修改文档