写在前面
本文从项目需求出发到项目最终发版提测,讲述一下项目中遇到的问题以及打怪升级过程(思路),文章中会提到涉及到的坑以及解决办法。相信看完,多少会给你提供一些价值。文章略长,分为上下二篇。
一、场景描述
兄弟们,来活了。接到一个项目需求,甲方要求部署项目时使用国产操作系统(麒麟),国产数据库(华为的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
最终的项目结构很简单,直接看代码
1、pom.xml
添加数据库驱动、工具类、dynamic-datasource(后面用到)
<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion>
<artifactId>mybatis-starter</artifactId>
<properties>
<starter.mybatis-spring.version>1.3.0</starter.mybatis-spring.version>
<starter.dynamic-datasource.version>3.5.2.companyName</starter.dynamic-datasource.version>
<jdbc.mysql.version>5.1.46</jdbc.mysql.version>
<jdbc.huaweicloud.dws.version>8.2.1</jdbc.huaweicloud.dws.version>
<hutool.version>5.6.3</hutool.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${starter.mybatis-spring.version}</version>
</dependency>
<!--工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!--动态数据源-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>${starter.dynamic-datasource.version}</version>
<!--排除和 mybatis-spring-boot-starter 中定义的版本冲突 -->
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-jdbc</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
<exclusion>
<artifactId>spring-boot-starter</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
<exclusion>
<artifactId>spring-aop</artifactId>
<groupId>org.springframework</groupId>
</exclusion>
</exclusions>
</dependency>
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${jdbc.mysql.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.huaweicloud.dws</groupId>
<artifactId>huaweicloud-dws-jdbc</artifactId>
<version>${jdbc.huaweicloud.dws.version}</version>
<scope>runtime</scope>
</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
去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(说明:实际上在后面的验证过程中,这个版本是有问题的
)
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-如何系统地看源码