springboot的多数据源实践

需求说明

最近接触到一个新的系统,隶属中台系统中的订单中心模块,目前现有的数据量已经远远超过1千万,而新的架构是舍弃掉了oracle,为了满足在mysql架构的查询效率问题和未来数据量下系统预留,本次会做成使用一致性哈希算法对订单号进行分表存储,减轻数据库压力,但是由于时间问题,对平台的订单进行全面替换难度很大而且不稳定,于是打算在原有的系统中添加多数据源,对数据进行双写,以减少迭代成本

本次项目架构

spring boot+Mybatis+oracle+mysql+pageHelper

实现目标

在项目中实现多数据源,并且可以在同一次请求中进行切换,将insert、update、delete操作在oracle和mysql两个库执行,对客和统计用的select语句可以继续在旧库上应用,等待迭代

####步骤说明

数据源定义

首先定义两个数据源,分别来自mysql和oracle,使项目可以获取多数据源的连接信息

mysql数据源配置

1
2
3
4
5
6
7
8
9
10
@Configuration
public class MysqlDataSourceConfig {
@Primary
@Bean(name = "mysqlDataSource")
@Qualifier("mysqlDataSource")
@ConfigurationProperties(prefix="spring.datasource.mysql")
public DataSource mysqlDataSource() {
return DataSourceBuilder.create().build();
}
}

oracle数据源配置

1
2
3
4
5
6
7
8
9
@Configuration
public class OracleDataSourceConfig {
@Bean(name = "oracleDataSource")
@Qualifier("oracleDataSource")
@ConfigurationProperties(prefix="spring.datasource.oracle")
public DataSource oracleDataSource() {
return DataSourceBuilder.create().build();
}
}
定义数据源枚举
1
2
3
4
5

public enum DataSourceEnum {
ORACLE,
MYSQL
}
定义ThreadLocal,用于指定线程中应该使用的数据源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class DataSourceType {
private static final ThreadLocal<DataSourceEnum> TYPE=new ThreadLocal<>();

/**
* 设置数据源信息
* @param dataSourceEnum
*/
public static void setSource(DataSourceEnum dataSourceEnum){
if(dataSourceEnum==null){
throw new NullPointerException("当前方法需要指定数据源!!");
}
TYPE.set(dataSourceEnum);
}

/**
* 获得数据源信息
* @return
*/
public static DataSourceEnum getSource(){
return TYPE.get()==null? DataSourceEnum.MYSQL:TYPE.get();
}

/**
* 清除数据源信息
*/
public static void clearSource(){
TYPE.remove();
}
}
自定义注解,指定为方法级注解,在方法上声明指定的数据源
1
2
3
4
5
6
7
8
9
10
11
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSourceMatch {
/**
* 选择数据库
* @return
*/
DataSourceEnum dbName() default DataSourceEnum.MYSQL;

}
自定义aop拦截切面,拦截有指定注解的方法,解析注解中指定的数据源并通过ThreadLocal进行设置和清除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Aspect
@Component
@Slf4j
public class DataSourceAspect {

@Pointcut("@annotation(com.xxx.DataSource.DataSourceMatch)")
public void setSource() {

}

@Around("setSource()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DataSourceMatch ds = method.getAnnotation(DataSourceMatch.class);
if (ds == null) {
DataSourceType.setSource(DataSourceEnum.MYSQL);
log.info("DataSource choose {}",DataSourceEnum.MYSQL);
} else {
DataSourceType.setSource(ds.dbName());
log.info("DataSource choose {}",ds.dbName());
}
try {
return point.proceed();
} finally {
log.info("DataSource clear ");
DataSourceType.clearSource();
}
}

}
实现spring的动态数据源接口AbstractRoutingDataSource
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final Logger log = LoggerFactory.getLogger(DynamicDataSource.class);
@Override
protected final DataSource determineTargetDataSource(){
DataSource dataSource=super.determineTargetDataSource();
log.info("切换到数据源{}",dataSource.getClass().getName());
return dataSource;

}
@Override
protected DataSourceEnum determineCurrentLookupKey() {
log.info("设置数据源为{}",DataSourceType.getSource());
return DataSourceType.getSource();
}
}
把AbstractRoutingDataSource交给IOC容器管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class DynamicDataSourceConfiguration {

@Bean("dynamicDataSource")
@Qualifier("dynamicDataSource")
public DataSource dynamicDataSource(@Qualifier("mysqlDataSource")DataSource mysqlDataSource, @Qualifier("oracleDataSource")DataSource oracleDataSource){
Map<Object,Object> dataSourceMap=new HashMap<>(4);
dataSourceMap.put(DataSourceEnum.MYSQL,mysqlDataSource);
dataSourceMap.put(DataSourceEnum.ORACLE,oracleDataSource);
DynamicDataSource dynamicDataSource=new DynamicDataSource();
//配置多数据源
dynamicDataSource.setTargetDataSources(dataSourceMap);
//默认使用新核心数据源
dynamicDataSource.setDefaultTargetDataSource(mysqlDataSource);
return dynamicDataSource;
}

}
springBoot配置,提供druid连接池配置和数据源信息配置

用<>符号标注了常用的修改项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
spring:
datasource:
mysql:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://<HOST>:3306/<DBname>
username: <NAME>
password: <PWD>
initialize: true
oracle:
driver-class-name: oracle.jdbc.driver.OracleDriver
url: jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=<HOST>)(PORT=1524)))(CONNECT_DATA=(SERVICE_NAME=<DBname>)))
username: <NAME>
password: <PWD>
druid:
initial-size: 30
minIdle: 30
maxActive: 200
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
filters: stat,wall,log4j
aop-patterns: com.xxx..service.*.*
remove-abandoned: true
use-local-session-state: true
use-global-data-source-stat: true
stat-view-servlet:
login-username: admin
login-password: admin
pagehelper分页工具配置,
1
2
3
4
5
6
7
8
pagehelper:
offset-as-page-num: true
row-bounds-with-count: true
reasonable: false
page-size-zero: true
params: pageNum=start;pageSize=limit;
#主要是这里需要开启方言的修改
autoRuntimeDialect: true

到这里就完成了所有的更改,在代码编写中,通过aop环绕增强,如果不在service级别指定数据源,则使用mysql数据源,如果指定是oracle则使用oracle数据源,通过ThreadLocal进行保存,spring的AbstractRoutingDataSource会帮我们获取保存在线程变量中的数据源并设置到本次Mybatis的查询sqlSession中,配合Mybatis的分页插件pageHelper自带的动态数据源切换自动修改分页方言功能,完成了项目里sql执行和自动分页功能,已经基本完成了使用需求。