diff --git a/README.md b/README.md index eae5864e..da9a8be3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/dynamic-datasource-spring-boot-common/src/main/java/com/baomidou/dynamic/datasource/spring/boot/autoconfigure/DynamicDataSourceProperties.java b/dynamic-datasource-spring-boot-common/src/main/java/com/baomidou/dynamic/datasource/spring/boot/autoconfigure/DynamicDataSourceProperties.java index cc0af076..4692442a 100644 --- a/dynamic-datasource-spring-boot-common/src/main/java/com/baomidou/dynamic/datasource/spring/boot/autoconfigure/DynamicDataSourceProperties.java +++ b/dynamic-datasource-spring-boot-common/src/main/java/com/baomidou/dynamic/datasource/spring/boot/autoconfigure/DynamicDataSourceProperties.java @@ -68,6 +68,10 @@ public class DynamicDataSourceProperties { * 是否懒加载数据源 */ private Boolean lazy = false; + /** + * 是否优雅关闭数据源,等待一段时间后再将数据源销毁 + */ + private Boolean graceDestroy = false; /** * seata使用模式,默认AT */ diff --git a/dynamic-datasource-spring-boot-starter/src/main/java/com/baomidou/dynamic/datasource/spring/boot/autoconfigure/DynamicDataSourceAutoConfiguration.java b/dynamic-datasource-spring-boot-starter/src/main/java/com/baomidou/dynamic/datasource/spring/boot/autoconfigure/DynamicDataSourceAutoConfiguration.java index d53c972d..ae90443f 100644 --- a/dynamic-datasource-spring-boot-starter/src/main/java/com/baomidou/dynamic/datasource/spring/boot/autoconfigure/DynamicDataSourceAutoConfiguration.java +++ b/dynamic-datasource-spring-boot-starter/src/main/java/com/baomidou/dynamic/datasource/spring/boot/autoconfigure/DynamicDataSourceAutoConfiguration.java @@ -71,6 +71,7 @@ public DataSource dataSource(List providers) { dataSource.setStrategy(properties.getStrategy()); dataSource.setP6spy(properties.getP6spy()); dataSource.setSeata(properties.getSeata()); + dataSource.setGraceDestroy(properties.getGraceDestroy()); return dataSource; } diff --git a/dynamic-datasource-spring-boot-starter/src/test/java/com/baomidou/dynamic/datasource/fixture/v1/AddRemoveDatasourceTest.java b/dynamic-datasource-spring-boot-starter/src/test/java/com/baomidou/dynamic/datasource/fixture/v1/AddRemoveDatasourceTest.java index b6893696..5204f256 100644 --- a/dynamic-datasource-spring-boot-starter/src/test/java/com/baomidou/dynamic/datasource/fixture/v1/AddRemoveDatasourceTest.java +++ b/dynamic-datasource-spring-boot-starter/src/test/java/com/baomidou/dynamic/datasource/fixture/v1/AddRemoveDatasourceTest.java @@ -53,6 +53,13 @@ void testAddAndRemoveDataSource() { 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"); diff --git a/dynamic-datasource-spring-boot3-starter/src/main/java/com/baomidou/dynamic/datasource/spring/boot/autoconfigure/DynamicDataSourceAutoConfiguration.java b/dynamic-datasource-spring-boot3-starter/src/main/java/com/baomidou/dynamic/datasource/spring/boot/autoconfigure/DynamicDataSourceAutoConfiguration.java index 27253340..c42a38aa 100644 --- a/dynamic-datasource-spring-boot3-starter/src/main/java/com/baomidou/dynamic/datasource/spring/boot/autoconfigure/DynamicDataSourceAutoConfiguration.java +++ b/dynamic-datasource-spring-boot3-starter/src/main/java/com/baomidou/dynamic/datasource/spring/boot/autoconfigure/DynamicDataSourceAutoConfiguration.java @@ -72,6 +72,7 @@ public DataSource dataSource(List providers) { dataSource.setStrategy(properties.getStrategy()); dataSource.setP6spy(properties.getP6spy()); dataSource.setSeata(properties.getSeata()); + dataSource.setGraceDestroy(properties.getGraceDestroy()); return dataSource; } diff --git a/dynamic-datasource-spring-boot3-starter/src/test/java/com/baomidou/dynamic/datasource/fixture/v3/AddRemoveDatasourceTest.java b/dynamic-datasource-spring-boot3-starter/src/test/java/com/baomidou/dynamic/datasource/fixture/v3/AddRemoveDatasourceTest.java index 2da9bce6..302fc176 100644 --- a/dynamic-datasource-spring-boot3-starter/src/test/java/com/baomidou/dynamic/datasource/fixture/v3/AddRemoveDatasourceTest.java +++ b/dynamic-datasource-spring-boot3-starter/src/test/java/com/baomidou/dynamic/datasource/fixture/v3/AddRemoveDatasourceTest.java @@ -53,6 +53,13 @@ void testAddAndRemoveDataSource() { 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"); diff --git a/dynamic-datasource-spring/src/main/java/com/baomidou/dynamic/datasource/DynamicRoutingDataSource.java b/dynamic-datasource-spring/src/main/java/com/baomidou/dynamic/datasource/DynamicRoutingDataSource.java index d719e85d..a37f8e3a 100644 --- a/dynamic-datasource-spring/src/main/java/com/baomidou/dynamic/datasource/DynamicRoutingDataSource.java +++ b/dynamic-datasource-spring/src/main/java/com/baomidou/dynamic/datasource/DynamicRoutingDataSource.java @@ -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 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 providers) { this.providers = providers; @@ -153,7 +155,7 @@ public synchronized void addDataSource(String ds, DataSource dataSource) { 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 synchronized void removeDataSource(String ds) { } 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 synchronized void removeDataSource(String ds) { public void destroy() { log.info("dynamic-datasource start closing ...."); for (Map.Entry 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 @@ private void checkEnv() { * * @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) { diff --git a/dynamic-datasource-spring/src/main/java/com/baomidou/dynamic/datasource/destroyer/DataSourceActiveDetector.java b/dynamic-datasource-spring/src/main/java/com/baomidou/dynamic/datasource/destroyer/DataSourceActiveDetector.java new file mode 100644 index 00000000..64818fd2 --- /dev/null +++ b/dynamic-datasource-spring/src/main/java/com/baomidou/dynamic/datasource/destroyer/DataSourceActiveDetector.java @@ -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); +} diff --git a/dynamic-datasource-spring/src/main/java/com/baomidou/dynamic/datasource/destroyer/DataSourceDestroyer.java b/dynamic-datasource-spring/src/main/java/com/baomidou/dynamic/datasource/destroyer/DataSourceDestroyer.java new file mode 100644 index 00000000..d71e4b6d --- /dev/null +++ b/dynamic-datasource-spring/src/main/java/com/baomidou/dynamic/datasource/destroyer/DataSourceDestroyer.java @@ -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); +} diff --git a/dynamic-datasource-spring/src/main/java/com/baomidou/dynamic/datasource/destroyer/DefaultDataSourceDestroyer.java b/dynamic-datasource-spring/src/main/java/com/baomidou/dynamic/datasource/destroyer/DefaultDataSourceDestroyer.java new file mode 100644 index 00000000..2fcecb61 --- /dev/null +++ b/dynamic-datasource-spring/src/main/java/com/baomidou/dynamic/datasource/destroyer/DefaultDataSourceDestroyer.java @@ -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 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 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); + } + } +} diff --git a/dynamic-datasource-spring/src/main/java/com/baomidou/dynamic/datasource/destroyer/Dhcp2DataSourceActiveDetector.java b/dynamic-datasource-spring/src/main/java/com/baomidou/dynamic/datasource/destroyer/Dhcp2DataSourceActiveDetector.java new file mode 100644 index 00000000..c5c5ab70 --- /dev/null +++ b/dynamic-datasource-spring/src/main/java/com/baomidou/dynamic/datasource/destroyer/Dhcp2DataSourceActiveDetector.java @@ -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()); + } +} diff --git a/dynamic-datasource-spring/src/main/java/com/baomidou/dynamic/datasource/destroyer/DruidDataSourceActiveDetector.java b/dynamic-datasource-spring/src/main/java/com/baomidou/dynamic/datasource/destroyer/DruidDataSourceActiveDetector.java new file mode 100644 index 00000000..aa197994 --- /dev/null +++ b/dynamic-datasource-spring/src/main/java/com/baomidou/dynamic/datasource/destroyer/DruidDataSourceActiveDetector.java @@ -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()); + } +} diff --git a/dynamic-datasource-spring/src/main/java/com/baomidou/dynamic/datasource/destroyer/HikariDataSourceActiveDetector.java b/dynamic-datasource-spring/src/main/java/com/baomidou/dynamic/datasource/destroyer/HikariDataSourceActiveDetector.java new file mode 100644 index 00000000..e4541a39 --- /dev/null +++ b/dynamic-datasource-spring/src/main/java/com/baomidou/dynamic/datasource/destroyer/HikariDataSourceActiveDetector.java @@ -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()); + } +}