feat: gracy destory datasource (#579)
* feat: allow delay close datasource --------- Co-authored-by: guop <guop0781@fingard.com>
This commit is contained in:
parent
b4a0ee219b
commit
03c7cffec0
@ -105,6 +105,7 @@ spring:
|
||||
dynamic:
|
||||
primary: master #设置默认的数据源或者数据源组,默认值即为master
|
||||
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
|
||||
grace-destroy: false #是否优雅关闭数据源,默认为false,设置为true时,关闭数据源时如果数据源中还存在活跃连接,至多等待10s后强制关闭
|
||||
datasource:
|
||||
master:
|
||||
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
|
||||
|
@ -68,6 +68,10 @@ public class DynamicDataSourceProperties {
|
||||
* 是否懒加载数据源
|
||||
*/
|
||||
private Boolean lazy = false;
|
||||
/**
|
||||
* 是否优雅关闭数据源,等待一段时间后再将数据源销毁
|
||||
*/
|
||||
private Boolean graceDestroy = false;
|
||||
/**
|
||||
* seata使用模式,默认AT
|
||||
*/
|
||||
|
@ -71,6 +71,7 @@ public class DynamicDataSourceAutoConfiguration implements InitializingBean {
|
||||
dataSource.setStrategy(properties.getStrategy());
|
||||
dataSource.setP6spy(properties.getP6spy());
|
||||
dataSource.setSeata(properties.getSeata());
|
||||
dataSource.setGraceDestroy(properties.getGraceDestroy());
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
|
@ -53,6 +53,13 @@ public class AddRemoveDatasourceTest {
|
||||
dataSourceProperty.setUrl("jdbc:h2:mem:test1");
|
||||
dataSourceProperty.setDriverClassName("org.h2.Driver");
|
||||
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
|
||||
// async destroy datasource
|
||||
ds.setGraceDestroy(true);
|
||||
ds.addDataSource(dataSourceProperty.getPoolName(), dataSourceCreator.createDataSource(dataSourceProperty));
|
||||
assertThat(ds.getDataSources().keySet()).contains("slave_1");
|
||||
ds.removeDataSource("slave_1");
|
||||
// close directly
|
||||
ds.setGraceDestroy(false);
|
||||
ds.addDataSource(dataSourceProperty.getPoolName(), dataSourceCreator.createDataSource(dataSourceProperty));
|
||||
assertThat(ds.getDataSources().keySet()).contains("slave_1");
|
||||
ds.removeDataSource("slave_1");
|
||||
|
@ -72,6 +72,7 @@ public class DynamicDataSourceAutoConfiguration implements InitializingBean {
|
||||
dataSource.setStrategy(properties.getStrategy());
|
||||
dataSource.setP6spy(properties.getP6spy());
|
||||
dataSource.setSeata(properties.getSeata());
|
||||
dataSource.setGraceDestroy(properties.getGraceDestroy());
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
|
@ -53,6 +53,13 @@ public class AddRemoveDatasourceTest {
|
||||
dataSourceProperty.setUrl("jdbc:h2:mem:test1");
|
||||
dataSourceProperty.setDriverClassName("org.h2.Driver");
|
||||
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
|
||||
// async destroy datasource
|
||||
ds.setGraceDestroy(true);
|
||||
ds.addDataSource(dataSourceProperty.getPoolName(), dataSourceCreator.createDataSource(dataSourceProperty));
|
||||
assertThat(ds.getDataSources().keySet()).contains("slave_1");
|
||||
ds.removeDataSource("slave_1");
|
||||
// close directly
|
||||
ds.setGraceDestroy(false);
|
||||
ds.addDataSource(dataSourceProperty.getPoolName(), dataSourceCreator.createDataSource(dataSourceProperty));
|
||||
assertThat(ds.getDataSources().keySet()).contains("slave_1");
|
||||
ds.removeDataSource("slave_1");
|
||||
|
@ -15,6 +15,8 @@
|
||||
*/
|
||||
package com.baomidou.dynamic.datasource;
|
||||
|
||||
import com.baomidou.dynamic.datasource.destroyer.DataSourceDestroyer;
|
||||
import com.baomidou.dynamic.datasource.destroyer.DefaultDataSourceDestroyer;
|
||||
import com.baomidou.dynamic.datasource.ds.AbstractRoutingDataSource;
|
||||
import com.baomidou.dynamic.datasource.ds.GroupDataSource;
|
||||
import com.baomidou.dynamic.datasource.ds.ItemDataSource;
|
||||
@ -30,12 +32,10 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -71,6 +71,8 @@ public class DynamicRoutingDataSource extends AbstractRoutingDataSource implemen
|
||||
private Boolean p6spy = false;
|
||||
@Setter
|
||||
private Boolean seata = false;
|
||||
@Setter
|
||||
private Boolean graceDestroy = false;
|
||||
|
||||
public DynamicRoutingDataSource(List<DynamicDataSourceProvider> providers) {
|
||||
this.providers = providers;
|
||||
@ -153,7 +155,7 @@ public class DynamicRoutingDataSource extends AbstractRoutingDataSource implemen
|
||||
this.addGroupDataSource(ds, dataSource);
|
||||
// 关闭老的数据源
|
||||
if (oldDataSource != null) {
|
||||
closeDataSource(ds, oldDataSource);
|
||||
closeDataSource(ds, oldDataSource, graceDestroy);
|
||||
}
|
||||
log.info("dynamic-datasource - add a datasource named [{}] success", ds);
|
||||
}
|
||||
@ -194,7 +196,7 @@ public class DynamicRoutingDataSource extends AbstractRoutingDataSource implemen
|
||||
}
|
||||
if (dataSourceMap.containsKey(ds)) {
|
||||
DataSource dataSource = dataSourceMap.remove(ds);
|
||||
closeDataSource(ds, dataSource);
|
||||
closeDataSource(ds, dataSource, graceDestroy);
|
||||
if (ds.contains(UNDERLINE)) {
|
||||
String group = ds.split(UNDERLINE)[0];
|
||||
if (groupDataSources.containsKey(group)) {
|
||||
@ -214,7 +216,7 @@ public class DynamicRoutingDataSource extends AbstractRoutingDataSource implemen
|
||||
public void destroy() {
|
||||
log.info("dynamic-datasource start closing ....");
|
||||
for (Map.Entry<String, DataSource> item : dataSourceMap.entrySet()) {
|
||||
closeDataSource(item.getKey(), item.getValue());
|
||||
closeDataSource(item.getKey(), item.getValue(), false);
|
||||
}
|
||||
log.info("dynamic-datasource all closed success,bye");
|
||||
}
|
||||
@ -268,28 +270,34 @@ public class DynamicRoutingDataSource extends AbstractRoutingDataSource implemen
|
||||
*
|
||||
* @param ds dsName
|
||||
* @param dataSource db
|
||||
* @param graceDestroy If true, close the connection after a delay.
|
||||
*/
|
||||
private void closeDataSource(String ds, DataSource dataSource) {
|
||||
private void closeDataSource(String ds, DataSource dataSource, boolean graceDestroy) {
|
||||
try {
|
||||
DataSource realDataSource = null;
|
||||
if (dataSource instanceof ItemDataSource) {
|
||||
((ItemDataSource) dataSource).close();
|
||||
realDataSource = ((ItemDataSource) dataSource).getRealDataSource();
|
||||
} else {
|
||||
if (seata) {
|
||||
if (dataSource instanceof DataSourceProxy) {
|
||||
DataSourceProxy dataSourceProxy = (DataSourceProxy) dataSource;
|
||||
dataSource = dataSourceProxy.getTargetDataSource();
|
||||
realDataSource = dataSourceProxy.getTargetDataSource();
|
||||
}
|
||||
}
|
||||
if (p6spy) {
|
||||
if (dataSource instanceof P6DataSource) {
|
||||
Field realDataSourceField = P6DataSource.class.getDeclaredField("realDataSource");
|
||||
realDataSourceField.setAccessible(true);
|
||||
dataSource = (DataSource) realDataSourceField.get(dataSource);
|
||||
realDataSource = (DataSource) realDataSourceField.get(dataSource);
|
||||
}
|
||||
}
|
||||
Method closeMethod = ReflectionUtils.findMethod(dataSource.getClass(), "close");
|
||||
if (closeMethod != null) {
|
||||
closeMethod.invoke(dataSource);
|
||||
}
|
||||
if (null != realDataSource) {
|
||||
DataSourceDestroyer destroyer = new DefaultDataSourceDestroyer();
|
||||
if (graceDestroy) {
|
||||
destroyer.asyncDestroy(ds, realDataSource);
|
||||
} else {
|
||||
destroyer.destroy(ds, realDataSource);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright © 2018 organization baomidou
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package com.baomidou.dynamic.datasource.destroyer;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
/**
|
||||
* Description
|
||||
* Detect if the datasource contains active connections
|
||||
*
|
||||
* @author alvinkwok
|
||||
* @since 2023/10/18
|
||||
*/
|
||||
public interface DataSourceActiveDetector {
|
||||
boolean containsActiveConnection(DataSource dataSource);
|
||||
|
||||
boolean support(DataSource dataSource);
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright © 2018 organization baomidou
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package com.baomidou.dynamic.datasource.destroyer;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
/**
|
||||
* Used to destroy sources
|
||||
*/
|
||||
public interface DataSourceDestroyer {
|
||||
|
||||
void asyncDestroy(String name, DataSource dataSource);
|
||||
|
||||
/**
|
||||
* Immediately destroy the data source
|
||||
*
|
||||
* @param realDataSource wait destroy data source
|
||||
*/
|
||||
void destroy(String name, DataSource realDataSource);
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright © 2018 organization baomidou
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package com.baomidou.dynamic.datasource.destroyer;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* Description
|
||||
* DefaultDataSourceDestroyer, support check hikari、druid and dhcp2
|
||||
*
|
||||
* @author alvinkwok
|
||||
* @since 2023/10/18
|
||||
*/
|
||||
@Slf4j
|
||||
public class DefaultDataSourceDestroyer implements DataSourceDestroyer {
|
||||
|
||||
private static final String THREAD_NAME = "close-datasource";
|
||||
|
||||
private static final long TIMEOUT_CLOSE = 10 * 1000;
|
||||
|
||||
private final List<DataSourceActiveDetector> detectors = new LinkedList<>();
|
||||
|
||||
public DefaultDataSourceDestroyer() {
|
||||
detectors.add(new HikariDataSourceActiveDetector());
|
||||
detectors.add(new DruidDataSourceActiveDetector());
|
||||
detectors.add(new Dhcp2DataSourceActiveDetector());
|
||||
}
|
||||
|
||||
|
||||
public void asyncDestroy(String name, DataSource dataSource) {
|
||||
log.info("dynamic-datasource start asynchronous task to close the datasource named [{}],", name);
|
||||
ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
|
||||
Thread thread = new Thread(r);
|
||||
thread.setName(THREAD_NAME);
|
||||
return thread;
|
||||
});
|
||||
executor.execute(() -> graceDestroy(name, dataSource));
|
||||
executor.shutdown();
|
||||
}
|
||||
|
||||
private void graceDestroy(String name, DataSource dataSource) {
|
||||
try {
|
||||
DataSourceActiveDetector detector = detectors.stream()
|
||||
.filter(x -> x.support(dataSource))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
long start = System.currentTimeMillis();
|
||||
while (detector == null || detector.containsActiveConnection(dataSource)) {
|
||||
// make sure the datasource close
|
||||
if (System.currentTimeMillis() - start > TIMEOUT_CLOSE) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(100L);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("dynamic-datasource check the datasource named [{}] contains active connection failed,", name, e);
|
||||
}
|
||||
destroy(name, dataSource);
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately destroy the data source
|
||||
*
|
||||
* @param realDataSource wait destroy data source
|
||||
*/
|
||||
public void destroy(String name, DataSource realDataSource) {
|
||||
Class<? extends DataSource> clazz = realDataSource.getClass();
|
||||
try {
|
||||
Method closeMethod = ReflectionUtils.findMethod(clazz, "close");
|
||||
if (closeMethod != null) {
|
||||
closeMethod.invoke(realDataSource);
|
||||
log.info("dynamic-datasource close the datasource named [{}] success,", name);
|
||||
}
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
log.warn("dynamic-datasource close the datasource named [{}] failed,", name, e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright © 2018 organization baomidou
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package com.baomidou.dynamic.datasource.destroyer;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
/**
|
||||
* Description
|
||||
* DHCP2 data source pool active detector.
|
||||
*
|
||||
* @author alvinkwok
|
||||
* @since 2023/10/18
|
||||
*/
|
||||
public class Dhcp2DataSourceActiveDetector implements DataSourceActiveDetector {
|
||||
@Override
|
||||
@SneakyThrows(ReflectiveOperationException.class)
|
||||
public boolean containsActiveConnection(DataSource dataSource) {
|
||||
int activeCount = (int) dataSource.getClass().getMethod("getNumActive").invoke(dataSource);
|
||||
return activeCount != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean support(DataSource dataSource) {
|
||||
return "org.apache.commons.dbcp2.BasicDataSource".equals(dataSource.getClass().getName());
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright © 2018 organization baomidou
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package com.baomidou.dynamic.datasource.destroyer;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
/**
|
||||
* Description
|
||||
* alibaba Druid data source pool active detector.
|
||||
*
|
||||
* @author alvinkwok
|
||||
* @since 2023/10/18
|
||||
*/
|
||||
public class DruidDataSourceActiveDetector implements DataSourceActiveDetector {
|
||||
@Override
|
||||
@SneakyThrows(ReflectiveOperationException.class)
|
||||
public boolean containsActiveConnection(DataSource dataSource) {
|
||||
int activeCount = (int) dataSource.getClass().getMethod("getActiveCount").invoke(dataSource);
|
||||
return activeCount != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean support(DataSource dataSource) {
|
||||
return "com.alibaba.druid.pool.DruidDataSource".equals(dataSource.getClass().getName());
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright © 2018 organization baomidou
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
package com.baomidou.dynamic.datasource.destroyer;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
/**
|
||||
* Description
|
||||
* Hikari data source pool active detector.
|
||||
*
|
||||
* @author alvinkwok
|
||||
* @since 2023/10/18
|
||||
*/
|
||||
public class HikariDataSourceActiveDetector implements DataSourceActiveDetector {
|
||||
@Override
|
||||
@SneakyThrows(ReflectiveOperationException.class)
|
||||
public boolean containsActiveConnection(DataSource dataSource) {
|
||||
Object hikariPoolMXBean = dataSource.getClass().getMethod("getHikariPoolMXBean").invoke(dataSource);
|
||||
int activeCount = null == hikariPoolMXBean
|
||||
? 0
|
||||
: (int) hikariPoolMXBean.getClass().getMethod("getActiveConnections").invoke(hikariPoolMXBean);
|
||||
return activeCount != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean support(DataSource dataSource) {
|
||||
return "com.zaxxer.hikari.HikariDataSource".equals(dataSource.getClass().getName());
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user