【SpringBoot】服务停止数据源的关闭时机

1  前言

微服务中我们会用到数据源,数据源中其实就是管理我们的数据库连接,对于数据库而言,连接数是很珍贵的资源,所以释放无用或者长时间空闲的连接显得很重要。

那么对于微服务比如我们的 SpringBoot 当服务启动的时候会初始化数据源,那么停止的时候,是如何关闭数据源,释放连接的呢?这节我们就来看看不同数据源下的连接释放时机。

这节我们主要从三种数据源看起:动态数据源(DynamicDataSource)、SpringBoot现在默认的数据源(HikariDataSource)、阿里的德鲁伊(DruidDataSource),看下三者是什么时候释放连接的。

2  释放方式 

我们站在服务的角度,也就是我们的应用角度,释放时机分主动(应用主动关闭数据库连接)和被动(数据库服务关闭连接)。

2.1  被动方式

我理解的每个数据库都有自己的空闲连接或者超时连接的释放,我查了下来自(通义千问):

不同数据库系统的默认空闲连接超时时间设置各不相同,以下是一些常见数据库的默认空闲超时时间概览:

  • MySQL: 默认的空闲连接超时时间是8小时,通过wait_timeout和interactive_timeout两个变量控制。这两个变量的默认值通常是28800秒(8小时)。

  • PostgreSQL: PostgreSQL默认没有明确的空闲超时设置。但可以通过设置idle_in_transaction_session_timeout参数来限制处于事务中但空闲的会话时间,其默认值是0,意味着没有限制。

  • Microsoft SQL Server: SQL Server的默认空闲超时由连接字符串中的Connection Lifetime或Idle Timeout参数决定,或者由数据库服务器的配置设置。如果不显式设置,通常没有默认的空闲超时限制。

  • Oracle: Oracle数据库的空闲超时时间可以通过配置Profile来设定,特别是通过修改IDLE_TIME参数。默认情况下,如果未特别配置,Oracle可能没有固定的空闲超时时间,需要根据实际的Profile设置来确定。

  • OceanBase: OceanBase的默认空闲连接超时时间也是8小时,通过wait_timeout和interactive_timeout设置,类似于MySQL。另外,OceanBase还提供了ob_trx_timeout来控制事务超时时间,默认值为100000000微秒(约100秒)。  

请注意,这些默认值可能会随数据库版本的不同而有所变化,且通常都可以通过数据库的配置文件或动态修改系统参数的方式来调整。在实际应用中,根据应用需求和资源管理策略,通常会根据具体情况调整这些参数。

被动方式的,我看 PGSQL 默认居然不设置,那我有疑问了,那我服务强制停止,那连接谁来释放呢?下边我会实际来操作,把它归到了主动方式里。

2.2  主动方式(正常)

主动方式我这里分开,一种是服务正常停止也叫优雅停止比如类似 kill -15,一种是非正常的比如强制停止服务类似 kill -9。

2.2.1  SpringBoot + ShutdownHook

在了解正常的服务停止时,我们得先回忆下 Runtime.getRuntime().addShutdownHook(...),这个是 JVM 退出时会执行的回调,这个之前看 SpringBoot 的启动过程或者 Spring 的上下文刷新的时候都有看过,这里我们简单看下:

在 SpringBoot 里的 Bean 实现了 DisposableBean 接口的 Bean都会得到执行,至于 SpringBoot 什么时候放进去这个回调的,是在 Springboot 刷新上下文的时候注册的 shutdownHook:

{@link org.springframework.boot.SpringApplication#refreshContext(ConfigurableApplicationContext)
private void refreshContext(ConfigurableApplicationContext context) {
    refresh(context);
    if (this.registerShutdownHook) {
        try {
            context.registerShutdownHook();
               }
        catch (AccessControlException ex) {
            // Not allowed in some environments.
       }*     }
}
@Override
public void registerShutdownHook() {
    if (this.shutdownHook == null) {
        // No shutdown hook registered yet.
        this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) {
                       @Override
           public void run() {
                synchronized (startupShutdownMonitor) {
                    doClose();
               }
           }        *         };
        Runtime.getRuntime().addShutdownHook(this.shutdownHook);
   }
}

doClose() 方法里的 destroyBeans(); 就会调用每个实现了 DisposableBean 接口的 destory 销毁方法。

2.2.2  DynamicDataSource下的关闭

动态数据源就是实现了销毁的接口:

public class DynamicRoutingDataSource extends AbstractRoutingDataSource implements InitializingBean, DisposableBean {
    ...
    // 释放每个数据源的连接
    public void destroy() throws Exception {
        log.info("dynamic-datasource start closing ....");
        Iterator var1 = this.dataSourceMap.entrySet().iterator();
        while(var1.hasNext()) {
            Entry<String, DataSource> item = (Entry)var1.next();
            DataSource dataSource = (DataSource)item.getValue();
            if (this.seata) {
                DataSourceProxy dataSourceProxy = (DataSourceProxy)dataSource;
                dataSource = dataSourceProxy.getTargetDataSource();
            }
            if (this.p6spy) {
                Field realDataSourceField = P6DataSource.class.getDeclaredField("realDataSource");
                realDataSourceField.setAccessible(true);
                dataSource = (DataSource)realDataSourceField.get(dataSource);
            }
            Class clazz = dataSource.getClass();
            try {
                // 调用每个数据源的 close 方法
                Method closeMethod = clazz.getDeclaredMethod("close");
                closeMethod.invoke(dataSource);
            } catch (NoSuchMethodException var6) {
                log.warn("dynamic-datasource close the datasource named [{}] failed,", item.getKey());
            }
        }
        log.info("dynamic-datasource all closed success,bye");
    }
}

效果如下图:

2.2.3  HikariDataSource下的关闭

(1)spring.datasource.hikari.idle-timeout 空闲时间设置,超过该设置会数据源会释放该连接,有个固定间隔的调度任务会来进行这项操作:

 (2)close 方法 关闭数据源:

这块有个小困惑,我发现服务正常停止的情况下,没有打印日志,我怀疑是不是默认情况下不会主动释放,而是跟那种非正常的系统会自动关闭 socket 类似。

2.2.4  DruidDataSource下的关闭

(1)spring.datasource.hikari.idle-timeout 空闲时间设置,超过该设置会数据源会释放该连接,有个固定间隔的调度任务会来进行这项操作:

 (2)close 方法 关闭数据源:

这个关闭我看见日志了,说明有人调用了 close,但是但是但是我得说三个但是我没找到哪里调用的......debug走不到我的断点,打开debug日志也看不到是谁调用的....这有点让我难解,还望有大佬知道的话告知下。

2.3  主动方式(强停/非正常)

像上面 PGSQl 默认空闲超时是不限制的,那么比如我服务启动后我初始化了10条连接,然后我直接 kill -9,直接强制停掉,那这10个连接就一直占着了么,tcp不释放的么?

我抓了下包,发现强制停止,是会发送 RST 的标志,告诉服务器关闭连接的:

我觉得是不是进程停止的时候,是不是会主动释放该进程所关联的全部 socket呢?我问了下通义千问:

直接强制停止一个Spring Boot微服务(或任何使用TCP连接的应用程序)时,由于进程没有机会执行正常的清理和关闭连接的操作,操作系统会介入以终止这些连接。在这种情况下,操作系统通常会向对端发送带有RST(Reset)标志的TCP段,通知对方连接异常终止。

好吧,具体原理咱就不知道了,知道的是强制停止能关闭连接,那非常好。

3  小结  

好啦,这节主要是想看下服务停止的时候,数据源的连接是如何释放的,有理解不对的地方还请指点哈。

posted @ 2024-05-22 20:50  酷酷-  阅读(158)  评论(0编辑  收藏  举报