使用AbstractRoutingDataSource实现多数据源 与 获取mapper上注解
背景
随着业务发展速度越来越快,数据的增长也呈现倍数级别增长,数据库的压力,对于查询和写入等所有操作,都依赖于主库,其实有一些对于时效性要求不高的场景,无需使用主库查询,可以使用从库来分摊主库的压力,提升数据库集群整体的吞吐量
处理思路
其实处理思路共有两种
1. 我们创建两个数据源,不同的数据源扫描不同的包.
也就是查询从库或者查询主库,是通过不同的mapper文件物理隔离的,优势就是开发简单,快速搭建,不用担心侵入原有逻辑。缺点也是显而易见的,如果一个sql在某些场景下需要主库查询,另外场景需要从库查询,那么我们要写两套sql,如果有新增字段等ddl操作时,我们要改两遍sql,由于比较简单,本文不做描述。
2. 使用动态数据源配置
在多个数据源之上,抽象出来一个虚拟的动态数据源,等到执行sql的前一步,我们才会选择哪个数据源执行,优点是能够更精细化的管控sql执行,缺点的话就是开发稍微复杂些
动态数据源的执行逻辑
我们需要先了解下spring的AbstractRoutingDataSource ,spring提供了一套数据库路由方案,我们重写determineCurrentLookupKey 方法,将想要的数据库返回即可。
以spring boot 为例
一、 mysql的基本配置
-
首先配置主库和从库的数据源,这个是前提
@Bean(name = "mysqlDataSource") @Primary public DataSource mysqlDataSource() { DruidDataSource dataSource = new DruidDataSource(); dataSource.setDriverClassName(driverClass); dataSource.setUrl(url); dataSource.setUsername(user); dataSource.setPassword(password); dataSource.setMaxActive(300); dataSource.setKeepAlive(true); dataSource.setMaxWait(60000); dataSource.setValidationQuery("SELECT 'x'"); dataSource.setMinEvictableIdleTimeMillis(300000); dataSource.setTestWhileIdle(true); dataSource.setTestOnBorrow(true); dataSource.setTestOnReturn(false); return dataSource; } /** * 从库主库数据源 */ @Bean(name = "mysqlSlaveDataSource") public DataSource mysqlSlaveDataSource() { DruidDataSource dataSource = new DruidDataSource(); dataSource.setDriverClassName(driverClass); dataSource.setUrl(url); dataSource.setUsername(readUser); dataSource.setPassword(passwordRead); dataSource.setMaxActive(300); dataSource.setKeepAlive(true); dataSource.setMaxWait(60000); dataSource.setValidationQuery("SELECT 'x'"); dataSource.setMinEvictableIdleTimeMillis(300000); dataSource.setTestWhileIdle(true); dataSource.setTestOnBorrow(true); dataSource.setTestOnReturn(false); return dataSource; }
-
配置动态数据源,将多数据源注入进动态数据源,并设置默认的数据源
@Bean(name = "dynamicDataSource") public DynamicDataSource dynamicDataSource(@Qualifier("mysqlDataSource") DataSource mysqlDataSource, @Qualifier("mysqlSlaveDataSource") DataSource mysqlSlaveDataSource) { Map<Object, Object> targetDataSources = Maps.newHashMap(); targetDataSources.put(DataSourceEnum.MASTER, mysqlDataSource); targetDataSources.put(DataSourceEnum.SLAVE, mysqlSlaveDataSource); return new DynamicDataSource(mysqlDataSource, targetDataSources); }
-
将动态数据源设置到sql的sessionFactory
@Bean(name = "mysqlSqlSessionFactory") @Primary public SqlSessionFactory masterSqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) throws Exception { final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); sessionFactory.setDataSource(dynamicDataSource); sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MysqlDataSourceConfig.MAPPER_LOCATION)); // 配置 org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true); configuration.setDefaultStatementTimeout(30); configuration.addInterceptor(new MybatisUMPInterceptor()); sessionFactory.setConfiguration(configuration); return sessionFactory.getObject(); }
二、 新增的配置
- 新增加库的枚举定义
package com.common.enums;
/**
* 数据源配置枚举
*
* @author wangzym
*/
public enum DataSourceEnum {
/**
* 使用主库
*/
MASTER(1, "主库"),
/**
* 使用从库
*/
SLAVE(2, "从库");
/**
* /**
* 编码
*/
private Integer code;
/**
* 描述
*/
private String desc;
DataSourceEnum(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
public Integer getCode() {
return this.code;
}
public String getDesc() {
return this.desc;
}
public static DataSourceEnum getByCode(Integer code) {
if (code == null) {
return null;
}
for (DataSourceEnum dataSourceEnum : DataSourceEnum.values()) {
if (dataSourceEnum.getCode().equals(code)) {
return dataSourceEnum;
}
}
return null;
}
}
- 动态数据源的定义,需要继承spring的AbstractRoutingDataSource
package com.test.datasource;
import com.alibaba.fastjson.JSON;
import com.test.SettleCheckException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.Map;
/**
* 动态切换数据源类
*
* @author wangzym
*/
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
//不符合配置条件直接中断启动流程
if (defaultTargetDataSource == null || targetDataSources == null || targetDataSources.isEmpty()) {
throw new SettleCheckException("默认数据源和可切换的数据源不允许为空!请检查 spring-config-datasource-druid.xml - dynamicDataSource 对应配置");
}
//设置默认数据源
super.setDefaultTargetDataSource(defaultTargetDataSource);
//设置用于切换的数据源
super.setTargetDataSources(targetDataSources);
//初始化本地的数据源记录
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
log.debug("当前线程使用SQL的数据库是:{}", JSON.toJSONString(DynamicDataSourceHolder.getDataSource()));
return DynamicDataSourceHolder.getDataSource();
}
}
-
新增注解
package com.test.datasource; import com.test.DataSourceEnum; import java.lang.annotation.*; /** * 切换数据源注解,默认连接主库 * 要在mapper上加此注解才可以 * <p> */ @Inherited @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Documented public @interface SwitchDataSource { DataSourceEnum name() default DataSourceEnum.MASTER; }
-
拦截器改造需要拦截sql,此方法可以直接拦截sql执行,此方法要重点理解,是mybatis的拦截方案,其中Intercepts可以通过不同的类型,来进行不同的拦截,此处拦截所有执行的sql,直接获取mapper上的注解会获取不到,需要通过反射的方式,来获取,需要重点注意,此处不是本文的重点,有兴趣可以了解mybatis源码。
package com.test.interceptor; import com.test.DuccConstants; import com.test.enums.DataSourceEnum; import com.test.datasource.DynamicDataSourceHolder; @Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}), @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),}) @Slf4j public class MybatisUMPInterceptor implements Interceptor { @Value("${test.current.env:pre}") private static String env; static { env = System.getProperty("current.env", "pre"); log.info("当前切面的环境为:{}", env); } @Override public Object intercept(Invocation invocation) throws Throwable { CallerInfo callerInfo = null; try { //1.处理数据源切换 SwitchDataSource chooseDataSource = getSwitchDbAnnotation((MappedStatement) invocation.getArgs()[0]); if (null != chooseDataSource && DuccConstants.allowedMultiDb()) { //默认会使用MASTER数据源 如果开启事务了,而且当前数据源不是MASTER,会强制切换为MASTER DynamicDataSourceHolder.setDataSource(chooseDataSource.name()); if (TransactionSynchronizationManager.isActualTransactionActive() && DataSourceEnum.SLAVE.equals(chooseDataSource.name())) { DynamicDataSourceHolder.setDataSource(DataSourceEnum.MASTER); } } else { DynamicDataSourceHolder.setDataSource(DataSourceEnum.MASTER); } //2.埋点监控 Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0]; //ump key可以自定义 String jKey = String.format("%s_MyBatis.%s", env, ms.getId()); if (DynamicDataSourceHolder.getDataSource() != null && DynamicDataSourceHolder.getDataSource().equals(DataSourceEnum.SLAVE)) { jKey = jKey + "_slave"; } callerInfo = Profiler.registerInfo(jKey, false, true); } catch (Exception ignore) { //ignore log.warn("切面执行出现异常:{}", ignore.getStackTrace(), ignore); } try { return invocation.proceed(); } catch (Throwable e) { try { if (null != callerInfo) { Profiler.functionError(callerInfo); } } catch (Exception ignore) { //ignore } throw e; } finally { try { if (null != callerInfo) { Profiler.registerInfoEnd(callerInfo); } } catch (Exception ignore) { //ignore } } } private SwitchDataSource getSwitchDbAnnotation(MappedStatement mappedStatement) { SwitchDataSource annotation = null; try { String id = mappedStatement.getId(); String className = id.substring(0, id.lastIndexOf(".")); String methodName = id.substring(id.lastIndexOf(".") + 1); final Method[] method = Class.forName(className).getMethods(); for (Method me : method) { if (me.getName().equals(methodName) && me.isAnnotationPresent(SwitchDataSource.class)) { return me.getAnnotation(SwitchDataSource.class); } } } catch (Exception ex) { log.error("", ex); } return annotation; } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } }
-
在mapper上打注解标记
@SwitchDataSource(name = DataSourceEnum.SLAVE) Long queryTaskOfShopCount(GeneralFeeDetailDTO generalFeeDetailDTO);
至此,mysql 动态路由到指定数据库就讲解完成。