前言

随着请求并发量不断增加,单个数据库难以承受高并发带来的压力。一个项目使用多个数据库的情况(无论是主从复制、读写分离,还是分布式数据库结构),变得越来越普遍。一般情况下,在使用springboot-mybatis项目中,整合多数据源有两种方法:分包和AOP。

一、分包方式

1、在application.properties中配置2个数据库

## test1 database
spring.datasource.test1.url=jdbc:mysql://localhost:3307/multipledatasource1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
spring.datasource.test1.username=root
spring.datasource.test1.password=root
spring.datasource.test1.driver-class-name=com.mysql.cj.jdbc.Driver
## test2 database
spring.datasource.test2.url=jdbc:mysql://localhost:3307/multipledatasource2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
spring.datasource.test2.username=root
spring.datasource.test2.password=root
spring.datasource.test2.driver-class-name=com.mysql.cj.jdbc.Driver

2、建立2个数据源的配置类

第1个数据源配置类,如下所示:

//表示这个类为一个配置类
@Configuration
// 配置mybatis的接口类放的地方
@MapperScan(basePackages = "com.mzd.multipledatasources.mapper.test01", sqlSessionFactoryRef = "test1SqlSessionFactory")
public class DataSourceConfig1 {
    // 将这个对象放入Spring容器中
    @Bean(name = "test1DataSource")
    // 表示这个数据源是默认数据源
    @Primary
    // 读取application.properties中的配置参数,映射成为一个对象。prefix表示配置参数的前缀。
    @ConfigurationProperties(prefix = "spring.datasource.test1")
    public DataSource getDateSource1() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "test1SqlSessionFactory")
    // 表示这个数据源是默认数据源
    @Primary
    // @Qualifier表示查找Spring容器中名字为test1DataSource的对象
    public SqlSessionFactory test1SqlSessionFactory(@Qualifier("test1DataSource") DataSource datasource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(datasource);
        bean.setMapperLocations(
                // 设置mybatis的xml所在位置
                new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/test01/*.xml"));
        return bean.getObject();
    }

    @Bean("test1SqlSessionTemplate")
    // 表示这个数据源是默认数据源
    @Primary
    public SqlSessionTemplate test1sqlsessiontemplate(
            @Qualifier("test1SqlSessionFactory") SqlSessionFactory sessionfactory) {
        return new SqlSessionTemplate(sessionfactory);
    }
}

第2个数据源配置类,如下所示:

@Configuration
@MapperScan(basePackages = "com.mzd.multipledatasources.mapper.test02", sqlSessionFactoryRef = "test2SqlSessionFactory")
public class DataSourceConfig2 {
    @Bean(name = "test2DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.test2")
    public DataSource getDateSource2() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "test2SqlSessionFactory")
    public SqlSessionFactory test2SqlSessionFactory(@Qualifier("test2DataSource") DataSource datasource)
            throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(datasource);
        bean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/test02/*.xml"));
        return bean.getObject();
    }

    @Bean("test2SqlSessionTemplate")
    public SqlSessionTemplate test2sqlsessiontemplate(
            @Qualifier("test2SqlSessionFactory") SqlSessionFactory sessionfactory) {
        return new SqlSessionTemplate(sessionfactory);
    }
}

注意:

a、@Primary注解必须要加。如果不加,spring将无法区分哪个为主数据源(默认数据源)。

b、mapper的接口、xml形式以及dao层,都需要两个分开,目录如下图:

springboot mybatis plus 多数据源 mysql sqllite springboot mybatis配置多数据源_mybatis

3、mapper的xml形式文件位置必须要配置

bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(“XXXX.xml”));

否则,会报异常:no statement (这种错误也可能是:在mapper的xml中,namespace与项目的路径不一致导致的)。

4、service注入dao对象

在service层中,根据不同的业务,注入不同的dao层。

5、主从复制、读写分离

如果是主从复制、读写分离,那么负责增删改的数据库必须是主库(master)。比如,test01负责增删改,test02负责查询,则test01必须是主库。

6、分布式结构

如果是分布式结构,那么需要不同模块操作各自的数据库。譬如,test01包下全是test01业务,test02全是test02业务,倘若test01中掺杂着test02的编辑操作,则会产生事务问题:test01中的事务无法控制test02中的事务。

二、AOP实现:

使用这种方式实现多数据源,必须要清楚两个知识点:AOP原理AbstractRoutingDataSource抽象类

AOP:相当于拦截器,只要是满足要求的动作,都会被拦截过来,然后在切面上进行一系列的相关操作。

AbstractRoutingDataSource:这个类是实现多数据源的关键,其作用就是动态切换数据源。首先,多个数据源配置在targetDataSources这个属性中;然后,根据determineCurrentLookupKey()这个方法,获取当前数据源在map中的key值;然后,使用determineTargetDataSource()方法动态获取当前数据源,如果当前数据源不存并且默认数据源也不存在,就抛出异常。(targetDataSources是AbstractRoutingDataSource的一个map类型的属性,其key表示每个数据源的名字,value为每个数据源)。

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
    // 多数据源map集合
    private Map<Object, Object> targetDataSources;
    // 默认数据源
    private Object defaultTargetDataSource;
    // 其实就是targetDataSources,afterPropertiesSet()方法会将targetDataSources赋值给resolvedDataSources
    private Map<Object, DataSource> resolvedDataSources;
    private DataSource resolvedDefaultDataSource;

    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        this.targetDataSources = targetDataSources;
    }

    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = this.determineCurrentLookupKey();
        DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }
        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        } else {
            return dataSource;
        }
    }

    protected abstract Object determineCurrentLookupKey();
}

1、创建一个切换数据源类型的类

创建一个切换数据源类型的类,使用ThreadLocal保障线程的安全性,每个线程之间不会相互影响。

public class DataSourceType {

    public enum DataBaseType {
        TEST01, TEST02
    }

    // 使用ThreadLocal保证线程安全
    private static final ThreadLocal<DataBaseType> typeHolder = new ThreadLocal<DataBaseType>();

    // 往当前线程里设置数据源类型
    public static void setDataBaseType(DataBaseType dataBaseType) {
        if (dataBaseType == null) {
            throw new NullPointerException();
        }
        System.err.println("[将当前数据源改为]:" + dataBaseType);
        typeHolder.set(dataBaseType);
    }

    // 获取数据源类型
    public static DataBaseType getDataBaseType() {
        DataBaseType dataBaseType = typeHolder.get() == null ? DataBaseType.TEST01 : typeHolder.get();
        System.err.println("[获取当前数据源的类型为]:" + dataBaseType);
        return dataBaseType;
    }

    // 清空数据类型
    public static void clearDataBaseType() {
        typeHolder.remove();
    }

}

2、定义一个动态数据源

定义一个动态数据源,继承AbstractRoutingDataSource抽象类,并重写determineCurrentLookupKey()方法。

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        DataSourceType.DataBaseType dataBaseType = DataSourceType.getDataBaseType();
        return dataBaseType;
    }

}

3.定义多个数据源

@Configuration
@MapperScan(basePackages = "com.mzd.multipledatasources.mapper", sqlSessionFactoryRef = "SqlSessionFactory")
public class DataSourceConfig {
    @Primary
    @Bean(name = "test1DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.test1")
    public DataSource getDateSource1() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "test2DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.test2")
    public DataSource getDateSource2() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "dynamicDataSource")
    public DynamicDataSource DataSource(@Qualifier("test1DataSource") DataSource test1DataSource,
            @Qualifier("test2DataSource") DataSource test2DataSource) {
        Map<Object, Object> targetDataSource = new HashMap<>();
        targetDataSource.put(DataSourceType.DataBaseType.TEST01, test1DataSource);
        targetDataSource.put(DataSourceType.DataBaseType.TEST02, test2DataSource);
        DynamicDataSource dataSource = new DynamicDataSource();
        dataSource.setTargetDataSources(targetDataSource);
        dataSource.setDefaultTargetDataSource(test1DataSource);
        return dataSource;
    }

    @Bean(name = "SqlSessionFactory")
    public SqlSessionFactory test1SqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
            throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dynamicDataSource);
        bean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/*.xml"));
        return bean.getObject();
    }
}

4.定义AOP

可以理解为:不同业务切换不同数据库的入口和出口。

@Aspect
@Component
public class DataSourceAop {
    @Before("execution(* com.mzd.multipledatasources.service..*.test01*(..))")
    public void setDataSource2test01() {
        System.err.println("test01业务");
        DataSourceType.setDataBaseType(DataBaseType.TEST01);
    }

    @Before("execution(* com.mzd.multipledatasources.service..*.test02*(..))")
    public void setDataSource2test02() {
        System.err.println("test02业务");
        DataSourceType.setDataBaseType(DataBaseType.TEST02);
    }

    @After("execution(* com.mzd.multipledatasources.service..*.test01*(..))")
    public void setDataSource2test01() {
        System.err.println("test01业务");
        DataSourceType. clearDataBaseType(DataBaseType.TEST01);
    }

    @After("execution(* com.mzd.multipledatasources.service..*.test02*(..))")
    public void setDataSource2test02() {
        System.err.println("test02业务");
        DataSourceType. clearDataBaseType(DataBaseType.TEST02);
    }
}

在执行线程扫描包匹配com.mzd.multipledatasources.service..*.test01*和com.mzd.multipledatasources.service..*.test02*时,分别执行不同的数据库切换动作。

如果觉得匹配com.mzd.multipledatasources.service..*.test01*和com.mzd.multipledatasources.service..*.test02*过于复杂,那么可以定义一个注解来标识不同的数据库。

项目目录结构为:

springboot mybatis plus 多数据源 mysql sqllite springboot mybatis配置多数据源_数据源_02