场景如下:
现在使用的是spring+mybatis+mysql 数据源只有一个,mysql的一个库;现在因为其中一个表dau_baseinfo的数据量太大,千万级别。页面查询实在太慢,所以准备把dau_baseinfo表迁移到clickhouse,此时就需要再引入一个数据源,即clickhouse对应的数据源
下面开始配置多数据源
第一步:创建一个DynamicDataSource的类,继承AbstractRoutingDataSource并重写determineCurrentLookupKey方法,代码如下:
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 从自定义的位置获取数据源标识
return DynamicDataSourceHolder.getDataSource();
}
}
第二步:创建DynamicDataSourceHolder用于持有当前线程中使用的数据源标识,代码如下:
public class DynamicDataSourceHolder {
/**
* 注意:数据源标识保存在线程变量中,避免多线程操作数据源时互相干扰
*/
private static final ThreadLocal THREAD_DATA_SOURCE = new ThreadLocal();
public static String getDataSource() {
return THREAD_DATA_SOURCE.get();
}
public static void setDataSource(String dataSource) {
THREAD_DATA_SOURCE.set(dataSource);
}
public static void clearDataSource() {
THREAD_DATA_SOURCE.remove();
}
}
第三步:增加clickhouse对应常量配置
在application.properties中增加如下clickhouse的配置:
#clickhouse
clickhouse.driver=ru.yandex.clickhouse.ClickHouseDriver
#test
clickhouse.url=jdbc:clickhouse://192.168.*.86:8123/bi
#online
#clickhouse.url=jdbc:clickhouse://172.18.**.250:8123/bi
clickhouse.username=****
clickhouse.password=****
#连接池初始化时创建的连接数
clickhouse.pool.minIdle=5
#最大空闲连接:连接池中容许保持空闲状态的最大连接数量,超过空闲连接将被标记为不可用,然后被释放
clickhouse.pool.maxIdle=20
#最大活动连接:连接池在同一时间能够分配的最大活动连接的数量
clickhouse.pool.maxActive=50
#最大等待时间:当没有可用连接时,连接池等待连接被归还的最大时间数(单位毫秒)
clickhouse.pool.maxWait=120000
#连接池中,连接的可空闲的时间,超过就被收回
clickhouse.pool.minEvictableIdleTimeMillis=6000
#标标记是否删除泄漏的连接
clickhouse.pool.removeAbandoned=true
#泄漏的连接可以被删除的超时时间值
clickhouse.pool.removeAbandonedTimeout=6000
第四步配置数据源
classpath*:/mybatis/*.xml
classpath*:/mybatis/*/*.xml
helperDialect = mysql
配置已经完成,下面就是对数据源的使用了
public List countDauNew(Map condtiotnMap){
//此处将数据源指定为dataSource2,即连接clickhouse
DynamicDataSourceHolder.setDataSource("dataSource2");
List rhShowInfoList = mapper.countDau(condtiotnMap);
//此处将数据源重置为dataSource此处防止内存泄漏,所以使用clear
DynamicDataSourceHolder.clearDataSource();
return rhShowInfoList;
}
如果觉得以上、方式比较麻烦,可以使用注解的方式:
--------------------------------------------------------------------------------------华丽的分割线--------------------------------------------------------------------
但是问题来了
1如果每次切换数据源时都调用DynamicDataSourceHolder.setDataSource("xxx")就显得十分繁琐了,而且代码量大了很容易会遗漏,后期维护起来也比较麻烦。
2正常方式下,是先设置数据源,执行sql,最后clear数据源,使用默认数据源;但是如果执行sql有问题,就会导致clear数据源的代码没有执行,会导致偶尔数据源混乱的问题。
解决方案是使用注解@DataSource("xxx")就指定访问数据源,然后配合aop来解决上面两个问题
首先,我们得定义一个名为Datasource的注解,代码如下:
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface Datasource {
/** 数据库类型:mysqlBI或clickhouseBI **/
String datasourceType() default DataSourceConstant.MYSQL_BOSS_BI;
}
上面datasourceType就是实际的数据源,默认可以指定
然后,定义AOP切面以便拦截所有带有注解@Datasource的方法,取出注解的值作为数据源标识放到DynamicDataSourceHolder的线程变量中:
public class DataSourceAspect {
static org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(DataSourceAspect.class);
/**
* 功能描述: 根据注解内容获取对应的数据源
*
*/
public void beforeAdvice(JoinPoint joinPoint) {
try {
Signature signature = joinPoint.getSignature();
String methodName = signature.getName();
MethodSignature methodSignature = (MethodSignature) signature;
// 被拦截的方法
Method method = methodSignature.getMethod();
//获取被拦截方法上面的 @Datasource注解的内容
if (method.getAnnotation(Datasource.class) == null) {
return;
}
String datasourceType = method.getAnnotation(Datasource.class).datasourceType();
if(StringUtils.isEmpty(datasourceType)){
return ;
}
DynamicDataSourceHolder.setDataSource(datasourceType);
} catch (Exception e) {
// 记录本地异常日志
logger.error("数据源处理失败", e);
}
}
/**
* 功能描述: 清除数据源信息,使用默认数据源
*
*/
public void afterAdvice(JoinPoint joinPoint) {
try {
// Signature signature = joinPoint.getSignature();
//
// String methodName = signature.getName();
// MethodSignature methodSignature = (MethodSignature) signature;
// // 被拦截的方法
// Method method = methodSignature.getMethod();
// //获取被拦截方法上面的 @Datasource注解的内容
// if (method.getAnnotation(Datasource.class) == null) {
// return;
// }
DynamicDataSourceHolder.clearDataSource();
} catch (Exception e) {
// 记录本地异常日志
logger.error("数据源处理失败", e);
}
}
}
复制代码
最后在spring配置文件中配置拦截规则就可以了,比如拦截service层或者dao层的所有方法:
OK,这样就可以直接在类或者方法上使用注解@Datasource来指定数据源,不需要每次都手动设置了。
示例代码如下:
@Service
public class RhCityService {
@Autowired
public RhCityMapper mapper;
/**
* 功能描述:按照层级查询城市,如果传入为null则查询全部
*
* @param:
* @return:
* @auther: mazhen
* @date: 2018/9/29 下午4:08
*/
@Datasource(datasourceType = DataSourceConstant.MYSQL_BOSS_BI)
public List listCityByLevel(String cityLevel) {
HashMap map = new HashMap<>();
map.put("cityLevel",cityLevel);
List rhCityDtos = mapper.listCityByLevel(map);
return rhCityDtos;
}
}
提示:注解@Datasource既可以加在方法上,也可以加在接口或者接口的实现类上,优先级别:方法>实现类>接口。也就是说如果接口、接口实现类以及方法上分别加了@DataSource注解来指定数据源,则优先以方法上指定的为准。
--------------------------------------------------------------------------------------------——————————————-----------------
2018年10月 最近发现又掉坑里了
发现有的时候aop没有生效,详细如下:
见:https://blog.csdn.net/h2604396739/article/details/102610610
--------------------------------------------------------------------------------------------------------------------------------------------------
近两天面试再次入坑,数据源动态切换 + transational 会有效吗?
不会,DataSource动态切换会失效,因为SpringManagedTransaction.getConnection()为空时,会从AbstractRoutingDataSource中获取数据源;但是用了transactional,会发现因为在getConnection之前由会获取到对应的数据源,所以AbstractRoutingDataSource失效。
动态数据源的配置的AOP切片上加入Order(1),让其先执行即可
多个aop可以指定执行顺序,@Order(1)加到切面上 或者 继承ordered方法。
具体可以参考:https://www.cnblogs.com/zhwbqd/p/3757060.html和https://my.oschina.net/HuifengWang/blog/304188