【连接池】-从源码到适配(上),你遇到过数据库连接池的问题吗?This connection has been closed

写在前面

  本文从项目需求出发到项目最终发版提测,讲述一下项目中遇到的问题以及打怪升级过程(思路),文章中会提到涉及到的坑以及解决办法。相信看完,多少会给你提供一些价值。文章略长,分为上下二篇。


目录

  • 写在前面
  • 一、场景描述
  • 二、技术方案
    • 1.环境说明
  • 三、实施步骤
    • (一)新建模块mybatis-starter
      • 1、pom.xml
      • 2、resource
      • 3、DatabaseVendor
      • 4、DatabaseVendorLoadUtils
      • 5、MyBatisAutoConfiguration
      • 6、修改不兼容SQL对应的mapper.xml文件
    • (二)引入dynamic-datasource-spring-boot-starter
      • 1、引入dynamic-datasource
      • 2、修改application-template.yml
  • 四、服务报错及解决
    • (一)问题产生
    • (二)数据库连接信息对比查询
    • (三)结论
  • 五、参考资料
  • 写在后面
  • 系列文章


一、场景描述

  兄弟们,来活了。接到一个项目需求,甲方要求部署项目时使用国产操作系统(麒麟),国产数据库(华为的GaussDB)。

简单了解了一下GaussDB,它的前身是PostgreSQL。说到PostgreSQL,多少了解一些,它的SQL语法和函数和MySQL数据库多少有点儿不太一样。果不其然,官方给出的GaussDB与MySQL语法差异

简单说一下目前项目,目前项目持久层使用的架构是MyBatis + MySQL,现在甲方要求使用GaussDB,那么首先要解决的一个问题就是数据库适配,因为语法有差异。

重新开发一套数据库适配版的项目吗?

重新开发一套的话,存在一个弊端:目前的项目已经在全国其他的地方均有部署,如果再开发一套,那想要增加功能和修复Bug,就需要2个版本都要重新改,难免要出错,另外也会增加测试的难度。
当然也可以做2个版本,GaussDB这个版本就当做定制化,不纳入标准化(本文不考虑这种情况)

那现在目标就很明确了,想要使用一套代码,还要兼容多种数据库产品,有没有什么办法呢?


二、技术方案

  网上查询资料,MyBatis利用databaseIdProvider属性可实现多数据库支持,MyBatis可以根据不同的数据库厂商执行不同的语句,这不正是我所需要的嘛?起个Demo测试一下,OK~

可是服务模块那么多,每个服务都拷贝一份,这不就造成了重复,这是一个问题。但是,这个好解决,抽取一个公共模块即可(之前代码并没有进行统一管理,各是各的,你懂得~)。当然,现在抽取出来,也是为了将来慢慢地将持久层相关的进行统一,比如驱动、MyBatis、数据库连接池、PageHelper等等,也为升级做准备。

抽取模块的另外一个原因是现在兼容GaussDB,不定哪天又要兼容其他数据库厂商,怎么办?显然要将数据库厂商抽取出来,做成可配置的。如果真有增加数据库厂商的那一天,只求需要增加一个数据库产品名称,全局生效。(哥们写代码,虽说不尽完美,但是至少不让它变得更烂

根据不同的数据库厂商查询不同的SQL,这个也搞定了。

但是,项目中具体使用哪一种数据库,这个该怎么指定呢?
简单说明一下,我们项目的架构是使用的spring-cloud-config-server的DB模式,数据库连接、密码等参数是在数据库中config_properties配置的。

由于最初版没人考虑会去兼容其他类型数据库,application.yml中只配置了url中的ip、用户名和密码,如下:

url: jdbc:mysql://${common.mysql.global.url}:3306/dbname?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
  driver-class-name: com.mysql.jdbc.Driver
  username: ${common.mysql.global.username}
  password: ${common.mysql.global.password}

现在要兼容GaussDB,那么要修改的就有jdbc:mysql为jdbc:postgresql、端口号、数据库驱动、数据库名(GaussDB中原有的数据库名变成了模式currentSchema=db)

本次改造涉及到全局、统计、大数据服务(分别是自己的数据库),这样配置就是3套,都得修改;另外如果是GaussdbDB数据库,变量名还叫common.mysql,是不是不太优雅会引起歧义。

按照上面的分析,直接把url抽取到数据库中修改,能不能实现?
能,这种方式就是用什么数据库就把数据库相关的参数修改一下。这样做有一个弊端是易出错(比如:不熟悉GaussDB的同学),另外要大改之前的配置。

有没有什么办法,不动原来的代码,我新增一个配置,比如 common.gaussdb.global.url,这样我在数据库中有2套配置,使用哪个我通过一个动态参数配置来进行生效,部署哪个环境只需要查询批量更改数据库账号密码连接即可。

有了想法,进行调研,MyBatis的团队,苞米豆已经提供了解决方案,根据配置动态加载数据源

至此,本次改造方案基本上已定,接下来就是具体实施。

1.环境说明

代码版本比较旧,一直也没有人进行更新

名称 说明
spring-boot-starter-parent版本 1.5.13.RELEASE
spring-cloud-dependencies版本 Edgware.SR4
mybatis-spring-boot-starter版本 1.3.0
dbcp2连接池版本 继承自spring-boot-starter-parent,使用的是 2.1.1

三、实施步骤

(一)新建模块mybatis-starter

【连接池】-从源码到适配(上),你遇到过数据库连接池的问题吗?This connection has been closed_第1张图片
最终的项目结构很简单,直接看代码

1、pom.xml

添加数据库驱动、工具类、dynamic-datasource(后面用到)


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>

    <artifactId>mybatis-starterartifactId>

    <properties>
        <starter.mybatis-spring.version>1.3.0starter.mybatis-spring.version>
        <starter.dynamic-datasource.version>3.5.2.companyNamestarter.dynamic-datasource.version>
        <jdbc.mysql.version>5.1.46jdbc.mysql.version>
        <jdbc.huaweicloud.dws.version>8.2.1jdbc.huaweicloud.dws.version>
        <hutool.version>5.6.3hutool.version>
    properties>

    <dependencies>
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>${starter.mybatis-spring.version}version>
        dependency>

        
        <dependency>
            <groupId>cn.hutoolgroupId>
            <artifactId>hutool-allartifactId>
            <version>${hutool.version}version>
        dependency>

        
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>dynamic-datasource-spring-boot-starterartifactId>
            <version>${starter.dynamic-datasource.version}version>
            
            <exclusions>
                <exclusion>
                    <artifactId>spring-boot-starter-jdbcartifactId>
                    <groupId>org.springframework.bootgroupId>
                exclusion>
                <exclusion>
                    <artifactId>spring-boot-starterartifactId>
                    <groupId>org.springframework.bootgroupId>
                exclusion>
                <exclusion>
                    <artifactId>spring-aopartifactId>
                    <groupId>org.springframeworkgroupId>
                exclusion>
            exclusions>
        dependency>

        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>${jdbc.mysql.version}version>
            <scope>runtimescope>
        dependency>

        <dependency>
            <groupId>com.huaweicloud.dwsgroupId>
            <artifactId>huaweicloud-dws-jdbcartifactId>
            <version>${jdbc.huaweicloud.dws.version}version>
            <scope>runtimescope>
        dependency>

    dependencies>
    
project>

❗️ 技巧:建议安装一个插件Maven Helper插件,很方便查看jar包是否有冲突,根据实际情况修改pom文件

2、resource

在 src/resources下新建一个META-INF文件夹

新建 spring.factories 文件,内容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.zhht.mybatis.config.MyBatisAutoConfiguration

新建 database-vendor-metadata.json 文件,用于配置数据库产品厂商的database的映射关系,内容如下:

[
  {
    "productName": "MySQL",
    "databaseId": "mysql"
  },
  {
    "productName": "PostgreSQL",
    "databaseId": "postgre"
  }
]

3、DatabaseVendor

创建实体,用于对应database-vendor-metadata.json文件

public class DatabaseVendor {

    /**
     * 产品名称
     */
    private String productName;

    /**
     * 唯一标识,对应Mapper.xml中的databaseId
     */
    private String databaseId;

    public String getProductName() {
        return productName;
    }

    public void setProductName(String productName) {
        this.productName = productName;
    }

    public String getDatabaseId() {
        return databaseId;
    }

    public void setDatabaseId(String databaseId) {
        this.databaseId = databaseId;
    }
}

4、DatabaseVendorLoadUtils

创建数据库厂商加载类,用于加载 database-vendor-metadata.json 中的内容到properties文件中

public class DatabaseVendorLoadUtils {

    private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseVendorLoadUtils.class);

    /**
     * 配置数据库厂商
     */
    private static final String PATH = "META-INF/database-vendor-metadata.json";

    private DatabaseVendorLoadUtils() {
    }

    public static Properties load() {
        Properties properties = new Properties();
        String vendorJson = IoUtil.readUtf8(new ClassPathResource(PATH).getStream());
        List<DatabaseVendor> databaseVendors = JSONUtil.toList(vendorJson, DatabaseVendor.class);
        for (DatabaseVendor db : databaseVendors) {
            properties.setProperty(db.getProductName(), db.getDatabaseId());
        }

        LOGGER.info("database-vendor - load vendor [{}] success", properties);
        return properties;
    }
}
❗️ 注意:
由于新建的这个模块mybatis-starter以后会通过jar包的方式让其他模块引用。
这里需要注意,当一个Jar包依赖另外一个Jar包的时候,文件需要通过流的方式读取(这里使用了hutool工具类),
不能通过path路径的方式,否则linux环境会读取不到文件的路径。

5、MyBatisAutoConfiguration

最关键的配置,配置DatabaseIdProvider

@Configuration
@EnableConfigurationProperties
public class MyBatisAutoConfiguration {

    /**
     * 支持多种数据库产品
     * @return
     */
    @Bean
    @ConditionalOnMissingBean
    public DatabaseIdProvider getDatabaseIdProvider() {
        VendorDatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
        databaseIdProvider.setProperties(DatabaseVendorLoadUtils.load());
        return databaseIdProvider;
    }

}

这样,别的其他服务,只需要依赖 mybatis-starter 即可,不需要再重新写这些配置,另外新增别的数据库厂商,只需要 database-vendor-metadata.json 配置文件中加一个映射关系即可。

就问题你优不优雅?
具体原理,请看系列文章。

6、修改不兼容SQL对应的mapper.xml文件

示例:IFNULL要修改成COALESCE
<select id="getTotal" resultType="java.lang.Integer">
   <choose>
      <when test="_databaseId == 'postgre'">
         select COALESCE(SUM(COALESCE(t.amount,0)),0)
      when>
      <otherwise>
         select IFNULL(SUM(IFNULL(t.amount,0)),0)
      otherwise>
   choose>
   from table t
   ....
select>

(二)引入dynamic-datasource-spring-boot-starter

https://github.com/baomidou/dynamic-datasource
【连接池】-从源码到适配(上),你遇到过数据库连接池的问题吗?This connection has been closed_第2张图片

去mvn仓库中找到适合自己的版本

https://mvnrepository.com/

因为spring-boot的版本是1.5.13 版本,这里找适合的版本,要通过Compile Dependencies 查看,这里选用dynamic-datasource的2.5.7版本(3.x版本的spring-boot依赖是1.5.3,比项目中spring-boot 1.5.13要高),这里选用低版本2.5.7(说明:实际上在后面的验证过程中,这个版本是有问题的
【连接池】-从源码到适配(上),你遇到过数据库连接池的问题吗?This connection has been closed_第3张图片

1、引入dynamic-datasource

把dynamic-datasource-spring-boot-starter添加到mybatis-starer模块中,
这样,别的服务依赖mybatis-starer模块,也就依赖了dynamic-datasource-spring-boot-starter

2、修改application-template.yml

数据库 config_properties 表中新增数据库连接common.gaussdb.global.url等参数,
更改原有yml配置

# 原有配置
spring:
    datasource:
    url: jdbc:mysql://${common.mysql.global.url}:3306/acs_irconfdb?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
    driver-class-name: com.mysql.jdbc.Driver
    username: ${common.mysql.global.username}
    password: ${common.mysql.global.password}
    type: org.apache.commons.dbcp2.BasicDataSource
    dbcp2:
      initial-size: 5
      min-idle: 1
      max-idle: 5
      max-total: 200
      max-wait-millis: 10000
      validation-query: SELECT 1
      test-on-borrow: true
      test-on-return: false
      test-while-idle: true
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 30000
      remove-abandoned-timeout: 200

# 修改为如下,其中的primary也是读取数据库中的值
spring:
  datasource:
    dynamic:
      primary: ${spring.datasource.dynamic.primary} #设置默认的数据源或者数据源组, 设置哪个则启用哪个
      strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常, false使用默认数据源
      datasource:
        mysql:
          url: jdbc:mysql://${common.mysql.global.url}:3306/dbname?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
          driver-class-name: com.mysql.jdbc.Driver
          username: ${common.mysql.global.username}
          password: ${common.mysql.global.password}
        postgresql:
          url: jdbc:postgresql://${common.gaussdb.global.url}:8000/${common.gaussdb.global.name}?currentSchema=dbname&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
          driver-class-name: org.postgresql.Driver
          username: ${common.gaussdb.global.username}
          password: ${common.gaussdb.global.password}
      dbcp2:
        initial-size: 5
        min-idle: 1
        max-idle: 5
        max-total: 200
        max-wait-millis: 10000
        validation-query: SELECT 1
        test-on-borrow: true
        test-on-return: false
        test-while-idle: true
        time-between-eviction-runs-millis: 60000
        min-evictable-idle-time-millis: 30000
        remove-abandoned-timeout: 200

走你,发测试环境。接下来就是按类型批量修改需要兼容的Mapper文件。

❗️ 技巧:此处idea的查询有一个坑,默认查找有“个数”限制,需要手动修改下,否则,会存在替换不全的问题。
具体如何配置,请看参考文件“关于idea全局搜索不全的坑”

四、服务报错及解决

(一)问题产生

好景不长,运行了一段时间,项目日志开始陆续出现如下问题:This connection has been closed

### Cause: org.postgresql.util.PSQLException: This connection has been closed.
; SQL []; This connection has been closed.; nested exception is org.postgresql.util.PSQLException: This connection has been closed.
        at org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:105)
        at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:73)
        at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:82)
        at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:82)
        at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:88)
        at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:440)
        at com.sun.proxy.$Proxy172.selectOne(Unknown Source)
        at org.mybatis.spring.SqlSessionTemplate.selectOne(SqlSessionTemplate.java:159)
        at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:87)
        at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:93)
        at com.sun.proxy.$Proxy266.selectByItemCodeOperationId(Unknown Source)

重启服务,正常。运行一段时间,又会断开。
这,什么鬼?貌似在说连接的问题。可是本次改造并没有改数据库连接池的信息额~

由于引入的变量还有GaussDB,莫非是GaussDB数据库服务端配置的问题,超出了连接数?
咋整?比一比MySQL数据库和GaussDB数据库连接的差异呗,说干就干。

(二)数据库连接信息对比查询

本次改造涉及到33个服务:28个是数据库相关的服务,5个是api相关的服务

// mysql环境查询SQL
-- max_connections	5000
-- mysqlx_max_connections	100
show variables like '%max_connection%';

-- 连接数
show global status like  'Threads%';
-- 结果
Variable_name	Value
Threads_cached	41
Threads_connected	65
Threads_created	350
Threads_running	2

select * from information_schema.processlist
where command != 'Sleep'

-- 49(总共是49个连接,基本上在50左右)
select sum(total)
from (
	select DB, count(*)as total from information_schema.processlist
	GROUP BY DB
	ORDER BY COUNT(*) DESC
) as temp

// GaussDB环境查询SQL
SELECT application_name, COUNT(*) 
FROM PG_STAT_ACTIVITY WHERE DATNAME='acs_db' AND application_name = 'PostgreSQL JDBC Driver'
GROUP BY application_name;

//结果(总共是235个连接,有点儿离谱)
application_name 		count
PostgreSQL JDBC Driver	235

SELECT 
substring(connection_info::json ->> 'driver_path' from '/([^/]+)/lib/') as client, 
count(*)
FROM PG_STAT_ACTIVITY WHERE DATNAME='acs_db'
AND application_name = 'PostgreSQL JDBC Driver'
GROUP BY client

// 结果
client count
服务A 	10
服务B	10
服务C 	10
服务D 	10
服务E 	10
服务F 	10
...

咦,怎么每个服务的连接数都是10个,巧了吗,这不是。兄弟们,这就有意思了。
我的yml配置 dbcp2.initial-size=5,可是连接的都是10,会不会是我配置的连接池没生效?

那我本地起一个项目,连接数会不会变呢?
启动本地项目,果然又多了10个。

本地连接,可以看到client是这样的,能本地启动看出连接变化,这就有搞头了。

{
    "driver_name":"JDBC",
    "driver_version":"(GaussDB 8.1.3 build 595adae0) compiled at 2023-03-25 18:07:25 commit 3629 last mr 5138 release",
    "driver_path":"C:\\Users\\admin\\.m2\\repository\\com\\huaweicloud\\dws\\huaweicloud-dws-jdbc\\8.2.1\\huaweicloud-dws-jdbc-8.2.1.jar",
    "os_user":"admin"
}

(三)结论

yml中配置的连接池确实是没有生效,但是项目使用了一个默认的什么连接池。
定位到问题,且问题能复现,基本上这个问题就被解决了80%。接下来,我们就去看看源码到底发生了什么事情,敬请期待下篇分解。


五、参考资料

GaussDB与MySQL语法差异
MyBatis利用databaseIdProvider属性实现多数据库支持
根据配置动态加载数据源
关于idea全局搜索不全的坑
加上图标!精致你的markdown


写在后面

  如果本文内容对您有价值或者有启发的话,欢迎点赞、关注、评论和转发。您的反馈和陪伴将促进我们共同进步和成长。


系列文章

【连接池】-从源码到适配(下)使用dynamic-datasource导致连接池没生效(升级版本)
【源码】-MyBatis-如何系统地看源码

你可能感兴趣的:(工作指北,数据库)