一、基础介绍
多数据源字面意思,比如说二个数据库,甚至不同类型的数据库。在用SpringBoot开发项目时,随着业务量的扩大,我们通常会进行数据库拆分或是引入其他数据库,从而我们需要配置多个数据源。
二、项目目录截图
三、多数据源SQL结构设计如下(简单的主从关系):
PS:创建两个库用于搭建项目中主从使用不同的数据库,表可以随意定义。
四、配置编码
1.数据源自定义注解,DataSource.java
/**
* 数据源自定义注解*/
@Target({ ElementType.METHOD, ElementType.TYPE })@Retention(RetentionPolicy.RUNTIME)@Documented
@Inherited
public @interfaceDataSource {
DataSourcesType name()defaultDataSourcesType.MASTER;
}
2.数据源类型枚举类定义,DataSourcesType.java
/**
* 数据源类型*/
publicenum DataSourcesType {/**
* 主库*/MASTER,/**
* 从库*/SLAVE
}
3.多数据源application.yml配置文件配置
# 数据源配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
druid:
master:
url: jdbc:mysql://127.0.0.1:3306/master?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password:123456slave:
enable: true
url: jdbc:mysql://127.0.0.1:3306/slave?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password:123456# 初始连接数
initialSize:5# 最小连接池数量
minIdle:10# 最大连接池数量
maxActive:20# 配置获取连接等待超时的时间
maxWait:60000# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis:60000# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis:300000# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis:900000validationQuery:SELECT 1 FROMDUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
# 打开PSCache,并且指定每个连接上PSCache的大小
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize:20# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙,此处是filter修改的地方
filters:
commons-log.connection-logger-name: stat,wall,log4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000# 合并多个DruidDataSource的监控数据
useGlobalDataSourceStat: true
# 配置 DruidStatFilter
web-stat-filter:
enabled: true
url-pattern: /*exclusions: .js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*
stat-view-servlet:
enabled: true
url-pattern: /druid/*
# IP 白名单,没有配置或者为空,则允许所有访问
allow: 127.0.0.1
# IP 黑名单,若白名单也存在,则优先使用
deny: 192.168.31.253
# 禁用 HTML 中 Reset All 按钮
reset-enable: false
# 登录用户名/密码
login-username: root
login-password: 123
# 慢SQL记录
filter:
stat:
enabled: true
# 慢SQL记录
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true
4.数据源配置文件属性定义,DataSourceProperties.java
/**
* 数据源配置文件*/
@Setter
@Configuration
@ConfigurationProperties(prefix ="spring.datasource.druid")publicclass DataSourceProperties {
privateintinitialSize;
privateintminIdle;
privateintmaxActive;
privateintmaxWait;
privateinttimeBetweenEvictionRunsMillis;
privateintminEvictableIdleTimeMillis;
privateintmaxEvictableIdleTimeMillis;
private String validationQuery;
private boolean testWhileIdle;
private boolean testOnBorrow;
private boolean testOnReturn;publicDruidDataSource setDataSource(DruidDataSource datasource) {
datasource.setInitialSize(initialSize);/** 配置初始化大小、最小、最大*/datasource.setInitialSize(initialSize);
datasource.setMaxActive(maxActive);
datasource.setMinIdle(minIdle);/** 配置获取连接等待超时的时间*/datasource.setMaxWait(maxWait);/** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒*/datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);/** 配置一个连接在池中最小、最大生存的时间,单位是毫秒*/datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);/**
* 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。*/datasource.setValidationQuery(validationQuery);/** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。*/datasource.setTestWhileIdle(testWhileIdle);/** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。*/datasource.setTestOnBorrow(testOnBorrow);/** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。*/datasource.setTestOnReturn(testOnReturn);returndatasource;
}
5.多数据源切换处理,DynamicDataSourceContextHolder.java
/**
* 数据源切换处理*/
publicclass DynamicDataSourceContextHolder {public static final Logger log =LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);/**
*此类提供线程局部变量。这些变量不同于它们的正常对应关系是每个线程访问一个线程(通过get、set方法),有自己的独立初始化变量的副本。*/private static final ThreadLocal contextHolder = new ThreadLocal<>();/**
* 设置当前线程的数据源变量*/
publicstatic void setDataSourceType(String dataSourceType) {log.info("已切换到{}数据源", dataSourceType);
contextHolder.set(dataSourceType);
}/**
* 获取当前线程的数据源变量*/
publicstatic String getDataSourceType() {returncontextHolder.get();
}/**
* 删除与当前线程绑定的数据源变量*/
publicstatic void removeDataSourceType() {
contextHolder.remove();
}
}
6.获取数据源(依赖于 spring) 定义一个类继承AbstractRoutingDataSource实现determineCurrentLookupKey方法,该方法可以实现数据库的动态切换,DynamicDataSource.java
/**
* 获取数据源(依赖于 spring) 定义一个类继承AbstractRoutingDataSource实现determineCurrentLookupKey方法,该方法可以实现数据库的动态切换*/
publicclass DynamicDataSource extends AbstractRoutingDataSource {publicstatic DynamicDataSource build() {returnnew DynamicDataSource();
}/**
* 获取与数据源相关的key
* 此key是Map resolvedDataSources 中与数据源绑定的key值
* 在通过determineTargetDataSource获取目标数据源时使用*/
@Overrideprotected Object determineCurrentLookupKey() {returnDynamicDataSourceContextHolder.getDataSourceType();
}
}
7.数据源核心配置类,DataSourceConfiguration.java
/**
* 数据源配置类*/
@Configuration
publicclass DataSourceConfiguration {/**
* 主库*/
@Bean
@ConfigurationProperties("spring.datasource.druid.master")publicDataSource masterDataSource(DataSourceProperties dataSourceProperties) {return dataSourceProperties.setDataSource(DruidDataSourceBuilder.create().build());
}/**
* 从库*/
@Bean
@ConditionalOnProperty( prefix = "spring.datasource.druid.slave", name = "enable", havingValue = "true")//是否开启数据源开关---若不开启 默认适用默认数据源
@ConfigurationProperties("spring.datasource.druid.slave")publicDataSource slaveDataSource(DataSourceProperties dataSourceProperties) {return dataSourceProperties.setDataSource(DruidDataSourceBuilder.create().build());
}/**
* 设置数据源*/
@Bean(name ="dynamicDataSource")@Primary
publicDynamicDataSource dynamicDataSource(DataSource masterDataSource, DataSource slaveDataSource) {
Map targetDataSources = new HashMap<>();
DynamicDataSource dynamicDataSource=DynamicDataSource.build();
targetDataSources.put(DataSourcesType.MASTER.name(), masterDataSource);
targetDataSources.put(DataSourcesType.SLAVE.name(), slaveDataSource);//默认数据源配置 DefaultTargetDataSource
dynamicDataSource.setDefaultTargetDataSource(masterDataSource);//额外数据源配置 TargetDataSources
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.afterPropertiesSet();returndynamicDataSource;
}
}
8.多数据源切面配置类,用于获取注解上的注解,进行动态切换数据源,DynamicDataSourceAspect.java
@Aspect
@Component
@Order(-1) // 保证该AOP在@Transactional之前执行
publicclass DynamicDataSourceAspect {
protected Logger logger=LoggerFactory.getLogger(getClass());@Pointcut("@annotation(com.fuzongle.tankboot.common.annotation.DataSource)"+ "|| @within(com.fuzongle.tankboot.common.annotation.DataSource)")publicvoid dsPointCut() {
}@Around("dsPointCut()")publicObject around(ProceedingJoinPoint point) throws Throwable {
Method targetMethod=this.getTargetMethod(point);
DataSource dataSource= targetMethod.getAnnotation(DataSource.class);//获取要切换的数据源if (dataSource != null) {
DynamicDataSourceContextHolder.setDataSourceType(dataSource.name().name());
}
try {returnpoint.proceed();
}
finally {//销毁数据源 在执行方法之后
DynamicDataSourceContextHolder.removeDataSourceType();
}
}/**
* 获取目标方法*/private Method getTargetMethod(ProceedingJoinPoint pjp) throws NoSuchMethodException {
Signature signature=pjp.getSignature();
MethodSignature methodSignature=(MethodSignature) signature;
Method agentMethod=methodSignature.getMethod();returnpjp.getTarget().getClass().getMethod(agentMethod.getName(), agentMethod.getParameterTypes());
}
}
9.编写业务逻辑,切换从库查询数据。
10.编写测试方法,调用查询业务,查看是否切换数据源是否生效。
PS:这种多数据源的动态切换确实可以解决数据的主从分库操作,但是却有一个致命的BUG,那就是事务不但失效而且无法实现
一致性,因为涉及到跨库,因此我们必须另想办法来实现事务的ACID原则
注意:
1.如果有任何不懂的地方可以关注公众号就可以加我微信,随时欢迎互相帮助。
2.技术交流群QQ:422167709。
3.如果希望学习更多,希望微信扫码,长按扫码,帮忙关注一下,举手之劳,当您无助的时候真的能帮你。非常感谢您关注公众号 "编程小乐"。