前言
最近在网上看到一篇介绍如何实现多数据源动态切换的文章,一时兴起,笔者便观察了一下目前公司代码对这块的应用,然后发现虽然不同微服务使用的数据库基本都是主从架构,但是它们对于数据源动态切换的实现方式都不一样,于是想着盘点总结一下。
场景应用盘点
场景1:一种库 + 一主一从 + MyBatis
架构
一种库,即只存在一种数据库,同时这个数据库拥一台主库和一台从库,整体架构如下图所示:
目标
执行SQL的时候自动切换主库或从库,实现读写分离
配置文件
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
master:
url: jdbc:mysql://xxxxxx:3306/test1?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://xxxxx:3307/test2?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
代码
定义一个动态数据源类实现AbstractRoutingDataSource接口,在determineCurrentLookupKey方法中通过获取DubboProviderFilter携带的值来实现动态切换数据源,也就是说,使用方可以通过DubboProviderFilter来指定使用主库还是从库,而DubboProviderFilter内部其实就是使用了ThreadLocal。
/**
* 根据AbstractRoutingDataSource路由到不同数据源中
**/
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultDataSource,Map<Object, Object> targetDataSources){
super.setDefaultTargetDataSource(defaultDataSource);
super.setTargetDataSources(targetDataSources);
}
@Override
protected Object determineCurrentLookupKey() {
if (Boolean.parseBoolean(DubboProviderFilter.getValue("readonly"))) {
return "slave";
}
return "master";
}
}
接着构建数据源即可
/**
* 构建数据源
**/
@Configuration
public class DateSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.druid.master")
public DataSource masterDataSource(){
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.druid.slave")
public DataSource slaveDataSource(){
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "dynamicDataSource")
@Primary
public DynamicDataSource createDynamicDataSource(){
Map<Object,Object> dataSourceMap = new HashMap<>();
DataSource defaultDataSource = masterDataSource();
dataSourceMap.put("master",defaultDataSource);
dataSourceMap.put("slave",slaveDataSource());
return new DynamicDataSource(defaultDataSource,dataSourceMap);
}
}
场景2:一种库 + 一主一从 + Sharding-JDBC + MyBatis
架构
场景2和场景1在架构上完全一致,只不过多用了Sharding-JDBC框架。
目标
执行SQL的时候自动切换主库或从库,实现读写分离。
配置文件
spring:
# Sharding-JDBC的配置
shardingsphere:
datasource:
# 数据源,这里配置两个
names: master,slave0
# 主库
master:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://xxxxxxxx:3306/db1?serverTimezone=GMT%2B8&characterEncoding=utf-8
username: root
password: 123456
# 从库
slave0:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://xxxxxxxx:3307/db1?serverTimezone=GMT%2B8&characterEncoding=utf-8
username: root
password: 123456
# 主从节点配置
masterslave:
# 从库负载均衡算法,内置两个值:RANDOM、ROUND_ROBIN
load-balance-algorithm-type: round_robin
# 主从的名称,要保证唯一
name: ms
# 指定主数据源
master-data-source-name: master
# 指定从数据源(可以多个)
slave-data-source-names: slave0
代码
不需要额外代码,配置好之后Sharding-JDBC便能自动解析SQL语句,如果是增删改则走主库,查询则走从库。
场景3:多种库 + 一主一从 + MyBatis
架构
多种库,顾名思义,即多个拥有不同数据表的库,而且这些库都有主库和从库。在我们的系统中有个模块专门负责提供报表查询服务,因此该应用要同时和不同业务的数据库建立连接。
目标
执行SQL的时候能够切换到对应的库,同时实现读写分离
配置文件
这里示例配置3种库,分别是order(订单)、user(用户)、promotion(营销)
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
order-master:
url: jdbc:mysql://xxxxxx:3306/test1?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
order-slave:
url: jdbc:mysql://xxxxx:3307/test2?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
user-master:
url: jdbc:mysql://xxxxxx:3306/test3?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
user-slave:
url: jdbc:mysql://xxxxx:3307/test4?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
promotion-master:
url: jdbc:mysql://xxxxxx:3306/test3?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
promotion-slave:
url: jdbc:mysql://xxxxx:3307/test4?characterEncoding=utf-8&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
代码
先实现AbstractRoutingDataSource接口;同时内置一个ThreadLocal,用来存放每次执行SQL语句的时候选中的数据源名称
public class DynamicDataSource extends AbstractRoutingDataSource {
//线程本地环境
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
/**
* 构造器
*/
public DynamicDataSource(DataSource defaultTargetDataSource, Map < Object, Object > targetDataSources) {
super.setDefaultTargetDataSource(defaultTargetDataSource);
super.setTargetDataSources(targetDataSources);
super.afterPropertiesSet();
}
/**
* 路由数据源
*/
@Override
protected Object determineCurrentLookupKey() {
return getDataSource();
}
/**
* 设置ThreadLocal数据源
*/
public static void setDataSource(String dataSource) {
contextHolder.set(dataSource);
}
/**
* 获取ThreadLocal数据源
*/
public static String getDataSource() {
return contextHolder.get();
}
/**
* 清除ThreadLocal数据源
*/
public static void clearDataSource() {
contextHolder.remove();
}
}
构建多个数据源
@Configuration
public class DynamicDataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.druid.order-master")
public DataSource orderMasterDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.druid.order-slave")
public DataSource orderSlaveDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.druid.user-master")
public DataSource userMasterDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.druid.user-slave")
public DataSource userSlaveDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.druid.promotion-master")
public DataSource promotionMasterDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@ConfigurationProperties("spring.datasource.druid.promotion-slave")
public DataSource promotionSlaveDataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@Primary
public DynamicDataSource dataSource(DataSource orderMasterDataSource, DataSource orderSlaveDataSource,
DataSource userMasterDataSource, DataSource userSlaveDataSource,
DataSource promotionMasterDataSource, DataSource promotionSlaveDataSource
) {
Map <Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("order-master", orderMasterDataSource);
targetDataSources.put("order-slave", orderSlaveDataSource);
targetDataSources.put("user-master", userMasterDataSource);
targetDataSources.put("user-slave", userSlaveDataSource);
targetDataSources.put("promotion-master", promotionMasterDataSource);
targetDataSources.put("promotion-slave", promotionSlaveDataSource);
return new DynamicDataSource(orderSlaveDataSource, targetDataSources);
}
}
自定义注解,用来标注dao层的某个方法或者类使用哪个数据源
@Target({
ElementType.TYPE,
ElementType.METHOD
})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSources {
String name() default "order-slave";
}
使用AOP切面结合上述自定义注解进行动态切换数据源。注解指定了对应的数据源,但是最终是通过方法名来决定选择主库还是从库。
@Aspect
@Component
public class DataSourceAspect implements Ordered {
/**
* dao层切面
*/
@Pointcut("execution(* com.xxx.xxx.xxx.dao..*(..))")
public void dao() {
}
@Around("dao()")
public Object around(ProceedingJoinPoint point) throws Throwable {
try {
// 数据源优先级: 1、方法上的 2、类上的
MethodSignature signature = (MethodSignature) point.getSignature();
Class clazz = (Class) AopUtils.getTargetClass(point.getTarget()).getGenericInterfaces()[0];
resolveDataSource(clazz, signature.getMethod());
return point.proceed();
} finally {
DynamicDataSource.clearDataSource();
}
}
/**
* 提取目标对象方法注解和类型注解中的数据源标识
*/
private void resolveDataSource(Class<?> clazz, Method method) throws Throwable {
// 默认使用订单从库
String sourceName = "order-slave";
// 类上的注解
DataSources source = clazz.getAnnotation(DataSources.class);
if (source != null) {
sourceName = source.name();
}
// 方法上的注解
if (method.isAnnotationPresent(DataSources.class)) {
source = method.getAnnotation(DataSources.class);
if (source != null) {
sourceName = source.name();
}
}
if (method.getName().startsWith("update") ||
method.getName().startsWith("batchUpdate") ||
method.getName().startsWith("batchWithBatch") ||
method.getName().startsWith("insert") ||
method.getName().startsWith("batchInsert") ||
method.getName().startsWith("batchDelete") ||
method.getName().startsWith("saveBatch") ||
method.getName().startsWith("set") ||
method.getName().startsWith("delete")) {
// 增删改语句则走主库
sourceName = sourceName.replace("slave", "master");
}
DynamicDataSource.setDataSource(sourceName);
}
@Override
public int getOrder() {
return 1;
}
}
dao层的mapper文件如下
@DataSources(name = "order-slave")
public interface OrderMapper {
@DataSources(name = "order-master")
List<StatOrderDailyPO> getDailyStatOrderByTimeRange(OrderDailyReqModel model);
@DataSources(name = "order-slave")
List<StatOrderDailyPO> countDailyStatOrderByTimeRange(OrderDailyReqModel model);
// 会被AOP切换为主库
int insertDailyStatOrderInfo(List<StatOrderDailyPO> list);
}
总结
3种场景,简单的说,要么实现AbstractRoutingDataSource接口,要么直接使用Sharding-JDBC框架。
通过对比上述场景1和场景2,可以看出,对于一种数据库并且是主从架构的情况,使用Sharding-JDBC效率更高,不用自己去实现读写分离数据源切换;而且如果是一主多从,Sharding-JDBC还支持对多个从库进行负载均衡。
笔者这里只列举了3种自己公司项目中使用多数据源的方法,并非所有场景都能适用这些方案,也希望这些例子能够帮到大家。