在日常开发中我们都是以单个数据库进行开发,在小型项目中是完全能够满足需求的。但是,当我们牵扯到像淘宝、京东这样的大型项目的时候,单个数据库就难以承受用户的CRUD操作。那么此时,我们就需要使用多个数据源进行读写分离的操作,这种方式也是目前一种流行的数据管理方式。
1 Spring Boot配置多数据源
在YAML文件中定义数据源所需的数据:
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
default-datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/default?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.jdbc.Driver
inspur-zs-datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/inspur-zs?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.jdbc.Driver
druid:
initial-size: 5
min-idle: 1
max-active: 20
profiles:
active: dev
mybatis:
mapper-locations: classpath:/mapper/*.xml
type-aliases-package: com.inspur.pojo
configuration:
mapUnderscoreToCamelCase: true
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
定义多个数据源:
package com.inspur.spring.config.datasource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
/**
* 数据源配置
* ConfigurationProperties注解用于将YAML中指定的数据创建成指定的对象,
* 但是,YAML中的数据必须要与对象对象中的属性同名,不然无法由Spring Boot完成赋值。
*
* @author zhaoshuai-lc
* @date 2023/07/11
*/
@Configuration
public class DataSourceConfig {
@Bean(name = "defaultDatasource")
@ConfigurationProperties(prefix = "spring.datasource.default-datasource")
public DataSource defaultDatasource() {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "inspurZsDatasource")
@ConfigurationProperties(prefix = "spring.datasource.inspur-zs-datasource")
public DataSource inspurZsDatasource() {
return DruidDataSourceBuilder.create().build();
}
}
由于我们要定义多个数据源,所以在Spring Boot数据源自动配置类中就无法确定导入哪个数据源来完成初始化,所以我们就需要禁用掉Spring Boot的数据源自动配置类,然后使用我们自定义的数据源配置类来完成数据源的初始化与管理。
package com.inspur;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* WtMybatisStudyApplication
* 由于我们要定义多个数据源,所以在Spring Boot数据源自动配置类中就无法确定导入哪个数据源来完成初始化,
* 所以我们就需要禁用掉Spring Boot的数据源自动配置类,然后使用我们自定义的数据源配置类来完成数据源的初始化与管理。
*
* @author zhaoshuai-lc
* @date 2023/07/11
*/
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableAspectJAutoProxy
public class WtMybatisStudyApplication {
public static void main(String[] args) {
SpringApplication.run(WtMybatisStudyApplication.class, args);
}
}
1.1 指定数据源-实现DataSource接口
缺点:产生大量的代码冗余,在代码中存在硬编码。
package com.inspur.spring.config.datasource;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
/**
* 方式一
* DynamicDataSource1
* 实现DataSource接口我们本质上只使用了一个方法就是getConnection()这个无参的方法
*
* @author zhaoshuai-lc
* @date 2023/07/11
*/
@Component
@Primary
public class DynamicDataSource1 implements DataSource {
public static ThreadLocal<String> flag = new ThreadLocal<>();
@Resource
private DataSource defaultDatasource;
@Resource
private DataSource inspurZsDatasource;
public DynamicDataSource1() {
flag.set("defaultDatasource");
}
@Override
public Connection getConnection() throws SQLException {
if (flag.get().equals("defaultDatasource")) {
return defaultDatasource.getConnection();
} else if (flag.get().equals("inspurZsDatasource")) {
return inspurZsDatasource.getConnection();
}
return defaultDatasource.getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return null;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
}
实现DataSource接口我们本质上只使用了一个方法,就是getConnection()这个无参的方法,但是DataSource接口中所有的方法我们也都需要实现,只是不用写方法体而已,也就是存在了很多的 “废方法” 。
@Primary注解 == @Order(1),用于设置此类的注入顺序。
使用:
@Override
public PageData<BsFactoryCalendar> selectByExample(BsFactoryCalendarExample example) {
PageData<BsFactoryCalendar> pageData = new PageData<>();
DynamicDataSource1.flag.set("default-datasource");
List<BsFactoryCalendar> bsFactoryCalendars = bsFactoryCalendarMapper.selectByExample(example);
PageInfo<BsFactoryCalendar> pageInfo = new PageInfo<>(bsFactoryCalendars);
pageData.setRows(bsFactoryCalendars);
pageData.setTotal(pageInfo.getTotal());
return pageData;
}
1.2 指定数据源-继承AbstrictRoutingDataSource类
减少了代码的冗余,但是还是会存在硬编码。
package com.inspur.spring.config.datasource;
import cn.hutool.core.map.MapUtil;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.Map;
/**
* DynamicDataSource2
* AbstrictRoutingDataSource的本质就是利用一个Map将数据源存储起来,然后通过Key来得到Value来修改数据源。
*
* @author zhaoshuai-lc
* @date 2023/07/11
*/
@Component
@Primary
public class DynamicDataSource2 extends AbstractRoutingDataSource {
public static ThreadLocal<String> flag = new ThreadLocal<>();
@Resource
private DataSource defaultDatasource;
@Resource
private DataSource inspurZsDatasource;
public DynamicDataSource2() {
flag.set("defaultDatasource");
}
@Override
protected Object determineCurrentLookupKey() {
return flag.get();
}
@Override
public void afterPropertiesSet() {
Map<Object, Object> targetDataSource = MapUtil.newConcurrentHashMap();
// 将第一个数据源设置为默认的数据源
super.setDefaultTargetDataSource(defaultDatasource);
targetDataSource.put("defaultDatasource", defaultDatasource);
targetDataSource.put("inspurZsDatasource", inspurZsDatasource);
super.setTargetDataSources(targetDataSource);
super.afterPropertiesSet();
}
}
AbstrictRoutingDataSource的本质就是利用一个Map将数据源存储起来,然后通过Key来得到Value来修改数据源。
使用:
@Override
public PageData<BsFactoryCalendar> selectByExample(BsFactoryCalendarExample example) {
PageData<BsFactoryCalendar> pageData = new PageData<>();
DynamicDataSource2.flag.set("default-datasource");
List<BsFactoryCalendar> bsFactoryCalendars = bsFactoryCalendarMapper.selectByExample(example);
PageInfo<BsFactoryCalendar> pageInfo = new PageInfo<>(bsFactoryCalendars);
pageData.setRows(bsFactoryCalendars);
pageData.setTotal(pageInfo.getTotal());
return pageData;
}
1.3 指定数据源-使用Spring AOP + 自定义注解的形式
Spring AOP + 自定义注解的形式是一种推荐的写法,减少代码的冗余且不存在硬编码。此方法适合对指定功能操作指定数据库的模式。
导入依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
开启AOP支持:
package com.inspur;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableAspectJAutoProxy
public class WtMybatisStudyApplication {
public static void main(String[] args) {
SpringApplication.run(WtMybatisStudyApplication.class, args);
}
}
定义枚举来表示数据源的标识:
package com.inspur.spring.config.datasource.enums;
public enum DataSourceType {
DEFAULT_DATASOURCE,
INSPURZS_DATASOURCE,
}
继承AbstractRoutingDataSource类:
package com.inspur.spring.config.datasource;
import cn.hutool.core.map.MapUtil;
import com.inspur.spring.config.datasource.enums.DataSourceType;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.Map;
@Primary
@Component
public class DynamicDataSource3 extends AbstractRoutingDataSource {
public static ThreadLocal<String> flag = new ThreadLocal<>();
@Resource
private DataSource defaultDatasource;
@Resource
private DataSource inspurZsDatasource;
public DynamicDataSource3() {
flag.set(DataSourceType.DEFAULT_DATASOURCE.name());
}
@Override
protected Object determineCurrentLookupKey() {
return flag.get();
}
@Override
public void afterPropertiesSet() {
Map<Object, Object> targetDataSource = MapUtil.newConcurrentHashMap();
// 将第一个数据源设置为默认的数据源
super.setDefaultTargetDataSource(defaultDatasource);
targetDataSource.put(DataSourceType.DEFAULT_DATASOURCE.name(), defaultDatasource);
targetDataSource.put(DataSourceType.INSPURZS_DATASOURCE.name(), inspurZsDatasource);
super.setTargetDataSources(targetDataSource);
super.afterPropertiesSet();
}
}
自定义注解:
package com.inspur.spring.config.datasource.annotation;
import com.inspur.spring.config.datasource.enums.DataSourceType;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
DataSourceType value() default DataSourceType.DEFAULT_DATASOURCE;
}
定义注解的实现类:
package com.inspur.spring.config.datasource.annotation;
import com.inspur.spring.config.datasource.DynamicDataSource3;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* TargetDataSourceAspect
*
* @author zhaoshuai-lc
* @date 2023/07/11
*/
@Component
@Aspect
@Slf4j
public class TargetDataSourceAspect {
@Before("@within(com.inspur.spring.config.datasource.annotation.TargetDataSource) || " +
"@annotation(com.inspur.spring.config.datasource.annotation.TargetDataSource)")
public void beforeNoticeUpdateDataSource(JoinPoint joinPoint) {
TargetDataSource annotation = null;
Class<? extends Object> target = joinPoint.getTarget().getClass();
if (target.isAnnotationPresent(TargetDataSource.class)) {
// 判断类上是否标注着注解
annotation = target.getAnnotation(TargetDataSource.class);
log.info("类: {}, 标注了注解@TargetDataSource", target);
} else {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
if (method.isAnnotationPresent(TargetDataSource.class)) {
// 判断方法上是否标注着注解,如果类和方法上都没有标注,则报错
annotation = method.getAnnotation(TargetDataSource.class);
log.info("方法: {}, 标注了注解@TargetDataSource", method);
} else {
log.error("注解@TargetDataSource只能用于类或者方法上, error: {} {}", target, method);
throw new RuntimeException("注解@TargetDataSource使用错误");
}
}
// 切换数据源
DynamicDataSource3.flag.set(annotation.value().name());
}
}
使用:
package com.inspur.spring.service;
import com.github.pagehelper.PageInfo;
import com.inspur.spring.common.interfaceResult.PageData;
import com.inspur.spring.config.datasource.annotation.TargetDataSource;
import com.inspur.spring.config.datasource.enums.DataSourceType;
import com.inspur.spring.dao.BsFactoryCalendarMapper;
import com.inspur.spring.pojo.BsFactoryCalendar;
import com.inspur.spring.pojo.BsFactoryCalendarExample;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
@Service
@TargetDataSource(value = DataSourceType.DEFAULT_DATASOURCE) // 方式三 多数据源设置
public class BsFactoryCalendarServiceImpl implements BsFactoryCalendarService {
@Resource
private BsFactoryCalendarMapper bsFactoryCalendarMapper;
@Override
@Transactional
public PageData<BsFactoryCalendar> selectByExample(BsFactoryCalendarExample example) {
PageData<BsFactoryCalendar> pageData = new PageData<>();
List<BsFactoryCalendar> bsFactoryCalendars = bsFactoryCalendarMapper.selectByExample(example);
PageInfo<BsFactoryCalendar> pageInfo = new PageInfo<>(bsFactoryCalendars);
pageData.setRows(bsFactoryCalendars);
pageData.setTotal(pageInfo.getTotal());
return pageData;
}
}
1.4 通过SqlSessionFactory指定的数据源来操作指定目录的XML文件
使用此方法则不会与上面所述的类有任何关系,本方法会重新定义类。本方法也是一种推荐的方法,适用于对指定数据库的操作,也就是适合读写分离。不会存在代码冗余和存在硬编码。
配置YAML文件:
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
default-datasource:
username: root
password: root
jdbc-url: jdbc:mysql://localhost:3306/default?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.jdbc.Driver
inspur-zs-datasource:
username: root
password: root
jdbc-url: jdbc:mysql://localhost:3306/inspur-zs?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.jdbc.Driver
main:
allow-bean-definition-overriding : true
druid:
initial-size: 5
min-idle: 1
max-active: 20
profiles:
active: dev
mybatis:
mapper-locations: classpath:/mapper/*.xml
type-aliases-package: com.inspur.pojo
configuration:
mapUnderscoreToCamelCase: true
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
针对Mapper层通过SqlSessionFactory指定数据源来操作:
package com.inspur.spring.config.datasource;
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.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.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
@Configuration
@MapperScan(basePackages = "com.inspur.spring.dao.defaultzs", sqlSessionFactoryRef = "DefaultSqlSessionFactory")
public class DefaultDatasourceConfig {
@Primary
@Bean(name = "DefaultDatasource")
@ConfigurationProperties(prefix = "spring.datasource.default-datasource")
public DataSource getDateSource1() {
return DataSourceBuilder.create().build();
}
@Primary
@Bean(name = "DefaultSqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("DefaultDatasource") DataSource datasource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(datasource);
// 设置mybatis的xml所在位置
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper/defaultzs/*.xml"));
return bean.getObject();
}
@Bean("DefaultSqlSessionTemplate")
@Primary
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("DefaultSqlSessionFactory") SqlSessionFactory factory) {
return new SqlSessionTemplate(factory);
}
@Bean
public PlatformTransactionManager transactionManager(@Qualifier("DefaultDatasource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
package com.inspur.spring.config.datasource;
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.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.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
@Configuration
@MapperScan(basePackages = "com.inspur.spring.dao.inspurzs", sqlSessionFactoryRef = "InspurZsSqlSessionFactory")
public class InspurZsDatasourceConfig {
@Primary
@Bean(value = "InspurZsDatasource")
@ConfigurationProperties(prefix = "spring.datasource.inspur-zs-datasource")
public DataSource getDateSource1() {
return DataSourceBuilder.create().build();
}
@Primary
@Bean(value = "InspurZsSqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("InspurZsDatasource") DataSource datasource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(datasource);
// 设置mybatis的xml所在位置
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper/inspurzs/*.xml"));
return bean.getObject();
}
@Bean(value = "InspurZsSqlSessionTemplate")
@Primary
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("InspurZsSqlSessionFactory") SqlSessionFactory factory) {
return new SqlSessionTemplate(factory);
}
@Bean
public PlatformTransactionManager transactionManager(@Qualifier("InspurZsDatasource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
@MapperScan注解中的basePackages指向的是指定的Dao层。
@MapperScan注解中sqlSessionFactoryRef 用来指定使用某个SqlSessionFactory来操作数据源。
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:mapper/inspurzs/*.xml"));
使用此种方法不会存在任何代码的冗余以及硬编码的存在,但是需要分层明确。唯一的不足就是添加一个数据源就需要重新写一个类,而这个类中的代码大部分又是相同的。