optimize 纯分页兼容mp和健康检查优化 (#305)

* 修改健康检查选择数据库的逻辑

修改健康检查的 引用类型转基本类型可能的报错。

* 修复 mybatis cache 开启后, 引起的 association 实效

* pom 默认构建doc,source

* 主从插件支持多主多从

* 给健康检查加入适配器, 可以处理: 开启健康检查没引入包的特殊情况
This commit is contained in:
Karen 2021-01-05 11:25:04 +08:00 committed by GitHub
parent ec7b5d7198
commit 96472635ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 268 additions and 58 deletions

View File

@ -61,10 +61,16 @@ public class GroupDataSource {
}
public DataSource determineDataSource() {
return dynamicDataSourceStrategy.determineDataSource(new ArrayList<>(dataSourceMap.values()));
String dsKey = determineDsKey();
return dataSourceMap.get(dsKey);
}
public String determineDsKey() {
return dynamicDataSourceStrategy.determineDSKey(new ArrayList<>(dataSourceMap.keySet()));
}
public int size() {
return dataSourceMap.size();
}
}

View File

@ -0,0 +1,34 @@
/**
* Copyright © 2018 organization baomidou
* <pre>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* <pre/>
*/
package com.baomidou.dynamic.datasource.exception;
/**
* exception when druid dataSource cannot select
*
* @author TaoYu
* @since 2.5.6
*/
public class CannotSelectDataSourceException extends RuntimeException {
public CannotSelectDataSourceException(String message) {
super(message);
}
public CannotSelectDataSourceException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -16,20 +16,30 @@
*/
package com.baomidou.dynamic.datasource.plugin;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.ds.GroupDataSource;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties;
import com.baomidou.dynamic.datasource.support.DbHealthIndicator;
import com.baomidou.dynamic.datasource.support.DdConstants;
import com.baomidou.dynamic.datasource.support.HealthCheckAdapter;
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.context.annotation.Lazy;
import javax.sql.DataSource;
import java.util.Map;
import java.util.Properties;
/**
@ -40,6 +50,7 @@ import java.util.Properties;
*/
@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 MasterSlaveAutoRoutingPlugin implements Interceptor {
@ -47,20 +58,25 @@ public class MasterSlaveAutoRoutingPlugin implements Interceptor {
@Autowired
private DynamicDataSourceProperties properties;
@Lazy
@Autowired(required = false)
private HealthCheckAdapter healthCheckAdapter;
@Autowired
protected DataSource dynamicDataSource;
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
boolean empty = true;
String pushedDataSource = null;
try {
empty = StringUtils.isEmpty(DynamicDataSourceContextHolder.peek());
if (empty) {
DynamicDataSourceContextHolder.push(getDataSource(ms));
}
String dataSource = getDataSource(ms);
pushedDataSource = DynamicDataSourceContextHolder.push(dataSource);
return invocation.proceed();
} finally {
if (empty) {
DynamicDataSourceContextHolder.clear();
if (pushedDataSource != null) {
DynamicDataSourceContextHolder.poll();
}
}
}
@ -72,25 +88,46 @@ public class MasterSlaveAutoRoutingPlugin implements Interceptor {
* @return 获取真实的数据源名称
*/
public String getDataSource(MappedStatement mappedStatement) {
String slave = DdConstants.SLAVE;
String currentDataSource = SqlCommandType.SELECT == mappedStatement.getSqlCommandType() ? DdConstants.SLAVE : DdConstants.MASTER;
String dataSource = null;
if (properties.isHealth()) {
/*
* 根据从库健康状况判断是否切到主库
*/
boolean health = DbHealthIndicator.getDbHealth(DdConstants.SLAVE);
if (!health) {
health = DbHealthIndicator.getDbHealth(DdConstants.MASTER);
DynamicRoutingDataSource dynamicRoutingDataSource = (DynamicRoutingDataSource) dynamicDataSource;
// 当前数据源是从库
if (DdConstants.SLAVE.equalsIgnoreCase(currentDataSource)) {
Map<String, GroupDataSource> currentGroupDataSources = dynamicRoutingDataSource.getCurrentGroupDataSources();
GroupDataSource groupDataSource = currentGroupDataSources.get(DdConstants.SLAVE);
String dsKey = groupDataSource.determineDsKey();
boolean health = healthCheckAdapter.getHealth(dsKey);
if (health) {
slave = DdConstants.MASTER;
dataSource = dsKey;
} else {
if (log.isWarnEnabled()) {
log.warn("从库无法连接, 请检查数据库配置, key: {}", dsKey);
}
}
}
// 从库无法连接, 或者当前数据源需要操作主库
if (dataSource == null) {
// 当前数据源是主库
Map<String, GroupDataSource> currentGroupDataSources = dynamicRoutingDataSource.getCurrentGroupDataSources();
GroupDataSource groupDataSource = currentGroupDataSources.get(DdConstants.MASTER);
dataSource = groupDataSource.determineDsKey();
boolean health = healthCheckAdapter.getHealth(dataSource);
if (!health) {
if (log.isWarnEnabled()) {
log.warn("主库无法连接, 请检查数据库配置, key: {}", dataSource);
}
}
}
} else {
dataSource = currentDataSource;
}
return SqlCommandType.SELECT == mappedStatement.getSqlCommandType() ? slave : DdConstants.MASTER;
return dataSource;
}
@Override
public Object plugin(Object target) {
return target instanceof Executor ? Plugin.wrap(target, this) : target;
return Plugin.wrap(target, this);
}
@Override

View File

@ -61,7 +61,7 @@ import java.util.Map;
@AllArgsConstructor
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
@Import(value = {DruidDynamicDataSourceConfiguration.class, DynamicDataSourceCreatorAutoConfiguration.class})
@Import(value = {DruidDynamicDataSourceConfiguration.class, DynamicDataSourceCreatorAutoConfiguration.class, DynamicDataSourceHealthCheckConfiguration.class})
@ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
public class DynamicDataSourceAutoConfiguration {

View File

@ -0,0 +1,58 @@
/**
* Copyright © 2018 organization baomidou
* <pre>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* <pre/>
*/
package com.baomidou.dynamic.datasource.spring.boot.autoconfigure;
import com.baomidou.dynamic.datasource.support.DbHealthIndicator;
import com.baomidou.dynamic.datasource.support.HealthCheckAdapter;
import org.springframework.boot.actuate.autoconfigure.ConditionalOnEnabledHealthIndicator;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
/**
* @author liushang@zsyjr.com
*/
@Configuration
public class DynamicDataSourceHealthCheckConfiguration {
private static final String DYNAMIC_HEALTH_CHECK = DynamicDataSourceProperties.PREFIX + ".health";
@Bean
public HealthCheckAdapter healthCheckAdapter() {
return new HealthCheckAdapter();
}
@ConditionalOnClass(AbstractHealthIndicator.class)
@ConditionalOnEnabledHealthIndicator("dynamicDS")
public class HealthIndicatorConfiguration {
@Bean("dynamicDataSourceHealthCheck")
@ConditionalOnProperty(DYNAMIC_HEALTH_CHECK)
public DbHealthIndicator healthIndicator(DataSource dataSource,
DynamicDataSourceProperties dynamicDataSourceProperties,
HealthCheckAdapter healthCheckAdapter) {
return new DbHealthIndicator(dataSource, dynamicDataSourceProperties.getHealthValidQuery(), healthCheckAdapter);
}
}
}

View File

@ -48,7 +48,7 @@ public class DynamicDataSourceProperties {
public static final String PREFIX = "spring.datasource.dynamic";
public static final String HEALTH = PREFIX + ".health";
public static final String DEFAULT_VALID_QUERY = "SELECT 1";
/**
* 必须设置默认的库,默认master
*/
@ -73,6 +73,10 @@ public class DynamicDataSourceProperties {
* 是否使用 spring actuator 监控检查默认不检查
*/
private boolean health = false;
/**
* 监控检查SQL
*/
private String healthValidQuery = DEFAULT_VALID_QUERY;
/**
* 每一个数据源
*/

View File

@ -17,6 +17,7 @@
package com.baomidou.dynamic.datasource.strategy;
import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.List;
/**
@ -35,5 +36,14 @@ public interface DynamicDataSourceStrategy {
* @param dataSources given dataSources
* @return final dataSource
*/
@Deprecated
DataSource determineDataSource(List<DataSource> dataSources);
/**
* determine a database from the given dataSources
*
* @param dsNames given dataSources
* @return final dataSource
*/
String determineDSKey(List<String> dsNames);
}

View File

@ -37,4 +37,9 @@ public class LoadBalanceDynamicDataSourceStrategy implements DynamicDataSourceSt
public DataSource determineDataSource(List<DataSource> dataSources) {
return dataSources.get(Math.abs(index.getAndAdd(1) % dataSources.size()));
}
@Override
public String determineDSKey(List<String> dsNames) {
return dsNames.get(Math.abs(index.getAndAdd(1) % dsNames.size()));
}
}

View File

@ -32,4 +32,9 @@ public class RandomDynamicDataSourceStrategy implements DynamicDataSourceStrateg
public DataSource determineDataSource(List<DataSource> dataSources) {
return dataSources.get(ThreadLocalRandom.current().nextInt(dataSources.size()));
}
@Override
public String determineDSKey(List<String> dsNames) {
return dsNames.get(ThreadLocalRandom.current().nextInt(dsNames.size()));
}
}

View File

@ -19,6 +19,7 @@ package com.baomidou.dynamic.datasource.support;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import org.springframework.boot.actuate.health.AbstractHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;
import org.springframework.dao.support.DataAccessUtils;
import org.springframework.jdbc.IncorrectResultSetColumnCountException;
import org.springframework.jdbc.core.JdbcTemplate;
@ -31,7 +32,6 @@ import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 数据库健康状况指标
@ -40,61 +40,63 @@ import java.util.concurrent.ConcurrentHashMap;
*/
public class DbHealthIndicator extends AbstractHealthIndicator {
/**
* 维护数据源健康状况
*/
private static final Map<String, Boolean> DB_HEALTH = new ConcurrentHashMap<>();
private final String validQuery;
private final HealthCheckAdapter healthCheckAdapter;
/**
* 当前执行数据源
*/
private final DataSource dataSource;
public DbHealthIndicator(DataSource dataSource) {
public DbHealthIndicator(DataSource dataSource, String validQuery,HealthCheckAdapter healthCheckAdapter) {
this.dataSource = dataSource;
this.validQuery = validQuery;
this.healthCheckAdapter = healthCheckAdapter;
}
/**
* 获取数据源连接健康状况
*
* @param dataSource 数据源名称
* @return 健康状况
*/
public static boolean getDbHealth(String dataSource) {
return DB_HEALTH.get(dataSource);
}
/**
* 设置连接池健康状况
*
* @param dataSource 数据源名称
* @param health 健康状况 false 不健康 true 健康
* @return 设置状态
*/
public static Boolean setDbHealth(String dataSource, boolean health) {
return DB_HEALTH.put(dataSource, health);
}
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
if (dataSource instanceof DynamicRoutingDataSource) {
Map<String, DataSource> dataSourceMap = ((DynamicRoutingDataSource) dataSource).getCurrentDataSources();
// 循环检查当前数据源是否可用
Boolean available = null;
Boolean disable = null;
for (Map.Entry<String, DataSource> dataSource : dataSourceMap.entrySet()) {
Integer result = 0;
Boolean resultAvailable = false;
try {
result = query(dataSource.getValue());
resultAvailable = queryAvailable(dataSource.getValue());
} catch (Throwable ignore){
} finally {
DB_HEALTH.put(dataSource.getKey(), 1 == result);
builder.withDetail(dataSource.getKey(), result);
healthCheckAdapter.putHealth(dataSource.getKey(), resultAvailable);
builder.withDetail(dataSource.getKey(), resultAvailable);
if (resultAvailable) {
available = true;
} else {
disable = true;
}
}
}
if (available != null) {
if (disable != null) {
builder.status(Status.OUT_OF_SERVICE);
} else {
builder.status(Status.UP);
}
}else{
builder.status(Status.DOWN);
}
}
}
private Integer query(DataSource dataSource) {
//todo 这里应该可以配置或者可重写
List<Integer> results = new JdbcTemplate(dataSource).query("SELECT 1", new RowMapper<Integer>() {
private Boolean queryAvailable(DataSource dataSource) {
List<Integer> results = new JdbcTemplate(dataSource).query(this.validQuery, new RowMapper<Integer>() {
@Override
public Integer mapRow(ResultSet resultSet, int i) throws SQLException {
@ -106,6 +108,6 @@ public class DbHealthIndicator extends AbstractHealthIndicator {
return (Integer) JdbcUtils.getResultSetValue(resultSet, 1, Integer.class);
}
});
return DataAccessUtils.requiredSingleResult(results);
return DataAccessUtils.requiredSingleResult(results) == 1;
}
}

View File

@ -0,0 +1,47 @@
/**
* Copyright © 2018 organization baomidou
* <pre>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* <pre/>
*/
package com.baomidou.dynamic.datasource.support;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author ls9527
*/
public class HealthCheckAdapter {
/**
* 维护数据源健康状况
*/
private static final Map<String, Boolean> DB_HEALTH = new ConcurrentHashMap<>();
public void putHealth(String key, Boolean healthState) {
DB_HEALTH.put(key,healthState);
}
/**
* 获取数据源连接健康状况
*
* @param dataSource 数据源名称
* @return 健康状况
*/
public boolean getHealth(String dataSource) {
Boolean isHealth = DB_HEALTH.get(dataSource);
return isHealth != null && isHealth;
}
}

View File

@ -65,8 +65,10 @@ public final class DynamicDataSourceContextHolder {
*
* @param ds 数据源名称
*/
public static void push(String ds) {
LOOKUP_KEY_HOLDER.get().push(StringUtils.isEmpty(ds) ? "" : ds);
public static String push(String ds) {
String dataSourceStr = StringUtils.isEmpty(ds) ? "" : ds;
LOOKUP_KEY_HOLDER.get().push(dataSourceStr);
return dataSourceStr;
}
/**