| @@ -0,0 +1,3 @@ | |||
| 1 | +-Dfile.encoding=UTF-8 | ||
| 2 | +-Dsun.stdout.encoding=UTF-8 | ||
| 3 | +-Dsun.stderr.encoding=UTF-8 | ||
| @@ -0,0 +1,135 @@ | |||
| 1 | +# spring-boot-ospp | ||
| 2 | + | ||
| 3 | +## 验证总览 | ||
| 4 | + | ||
| 5 | +| 序号 | 验证项 | 为什么验证 | 是否通过 | | ||
| 6 | +| --- | --- | --- | --- | | ||
| 7 | +| 1 | Spring Boot 自动装配 HikariCP | 确认 Spring Boot 2.5.6 引入 `spring-boot-starter-jdbc` 后,实际使用的是 `HikariDataSource`,不是其他连接池。 | 通过 | | ||
| 8 | +| 2 | HikariCP 5.x 运行版本 | 确认工程没有使用 Spring Boot 2.5.6 默认的 HikariCP 4.x,而是显式覆盖到 HikariCP 5.1.0。 | 通过 | | ||
| 9 | +| 3 | HikariCP 连接池参数 | 确认 `maximumPoolSize`、`minimumIdle`、`connectionTimeout`、`idleTimeout`、`maxLifetime`、`keepaliveTime` 等配置已被读取并生效。 | 通过 | | ||
| 10 | +| 4 | MySQL JDBC 基础连接 | 确认 MySQL Connector/J 可以通过 dolphin MySQL 协议端口连接 openGauss,并通过 `isValid`、`SELECT 1`、`version()` 验证基础 SQL 能力。 | 通过 | | ||
| 11 | +| 5 | dolphin 插件与协议配置 | 通过 `pg_extension` 验证 dolphin 插件已安装,通过 `pg_settings` 验证 `enable_dolphin_proto=on`、`dolphin_server_port=3306`。 | 通过 | | ||
| 12 | +| 6 | 客户端库名与 schema 映射 | 确认 JDBC URL 中的 `mysql_test_db` 能映射到 openGauss 当前 schema,避免文档中库名/schema 映射说明不严谨。 | 通过 | | ||
| 13 | +| 7 | 初始业务表数据 | 确认文档准备步骤创建的 `user` 表可查,并且包含 `张三`、`李四`、`王五` 三条基础数据。 | 通过 | | ||
| 14 | +| 8 | INSERT / UPDATE / DELETE | 确认通过 MySQL 协议连接 openGauss 后,常规写入、更新、删除链路都可用。 | 通过 | | ||
| 15 | +| 9 | Spring 事务提交 | 确认 `TransactionTemplate` 能基于 HikariCP 获取的连接正常提交事务,并且提交后的数据可查询。 | 通过 | | ||
| 16 | +| 10 | 连接池最大连接数限制 | 借满 `maximumPoolSize=10` 条连接后,再申请第 11 条连接,确认它会等待空闲连接,不会突破连接池最大连接数。 | 通过 | | ||
| 17 | +| 11 | 连接池等待恢复 | 释放 1 条已占用连接后,确认等待中的第 11 条连接可以恢复执行并完成 SQL。 | 通过 | | ||
| 18 | +| 12 | 真实并发数据库操作 | 启动 10 个 worker,每个 worker 持有自己的连接并完成 `insert/select/update/select/delete/commit`,证明不是只拿到连接就算并发通过。 | 通过 | | ||
| 19 | +| 13 | 最终清理 | 删除验证过程产生的临时数据,确认基础数据仍保留,保证重复运行结果可控。 | 通过 | | ||
| 20 | + | ||
| 21 | +Spring Boot 2.5.6 + HikariCP + MySQL Connector/J 验证工程,用于按 openGauss docs 的《基于 HikariCP 开发》验证 openGauss B 兼容库的 MySQL 协议连接能力。 | ||
| 22 | + | ||
| 23 | +## 环境 | ||
| 24 | + | ||
| 25 | +- JDK 11 | ||
| 26 | +- Spring Boot 2.5.6 | ||
| 27 | +- MySQL Connector/J 8.0.20 | ||
| 28 | +- HikariCP 5.1.0,通过 `hikaricp.version` 显式覆盖 Spring Boot 2.5.6 默认依赖版本 | ||
| 29 | +- Spring Boot `spring-boot-starter-jdbc` 默认连接池 HikariCP | ||
| 30 | + | ||
| 31 | +## openGauss 侧准备 | ||
| 32 | + | ||
| 33 | +按文档完成服务端准备: | ||
| 34 | + | ||
| 35 | +1. 开启 MySQL 协议兼容,确认 `enable_dolphin_proto=on`。 | ||
| 36 | +2. 创建 B 兼容库并加载 dolphin,示例库名为 `proto_test_db`。 | ||
| 37 | +3. 设置 `dolphin_server_port=3306` 并重启。 | ||
| 38 | +4. 在 B 库中创建 schema 和 `user` 表,schema 名为 `mysql_test_db`。 | ||
| 39 | +5. 创建同名连接用户 `mysql_test_db`,并执行 `SELECT set_native_password('mysql_test_db', '<password>', '');`。 | ||
| 40 | +6. 配置客户端接入认证,例如 `host all mysql_test_db 0.0.0.0/0 sha256`。 | ||
| 41 | + | ||
| 42 | +## 运行验证 | ||
| 43 | + | ||
| 44 | +默认会直接连接本机 openGauss dolphin MySQL 协议端口并执行验证: | ||
| 45 | + | ||
| 46 | +```powershell | ||
| 47 | +mvn spring-boot:run | ||
| 48 | +``` | ||
| 49 | + | ||
| 50 | +也可以直接在 IDEA 中运行 `com.linyu.ospp.SpringBootOsppApplication`。 | ||
| 51 | + | ||
| 52 | +默认连接信息: | ||
| 53 | + | ||
| 54 | +```text | ||
| 55 | +jdbc:mysql://127.0.0.1:3306/mysql_test_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf-8&allowPublicKeyRetrieval=true | ||
| 56 | +username=mysql_test_db | ||
| 57 | +password=xxxxxx | ||
| 58 | +``` | ||
| 59 | + | ||
| 60 | +如需覆盖连接信息,可设置环境变量: | ||
| 61 | + | ||
| 62 | +```powershell | ||
| 63 | +$env:OPENGAUSS_MYSQL_URL="jdbc:mysql://127.0.0.1:3306/mysql_test_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf-8&allowPublicKeyRetrieval=true" | ||
| 64 | +$env:OPENGAUSS_MYSQL_USERNAME="mysql_test_db" | ||
| 65 | +$env:OPENGAUSS_MYSQL_PASSWORD="xxxxxx" | ||
| 66 | +mvn spring-boot:run | ||
| 67 | +``` | ||
| 68 | + | ||
| 69 | +验证内容: | ||
| 70 | + | ||
| 71 | +- Spring Boot 是否自动装配 `HikariDataSource` | ||
| 72 | +- HikariCP 运行版本是否为 5.x | ||
| 73 | +- HikariCP 参数是否生效 | ||
| 74 | +- `SELECT 1` 和 `version()` 是否可执行 | ||
| 75 | +- `pg_extension` 中是否已安装 dolphin 插件,`enable_dolphin_proto` / `dolphin_server_port` 是否符合预期 | ||
| 76 | +- dolphin MySQL 协议下客户端库名 `mysql_test_db` 是否映射到当前 schema | ||
| 77 | +- 文档中的 `user` 表是否可查询 | ||
| 78 | +- INSERT / UPDATE / DELETE 是否可执行 | ||
| 79 | +- Spring 事务提交是否正常 | ||
| 80 | +- 连接数超过 HikariCP `maximumPoolSize` 后是否进入等待,释放连接后是否恢复执行 | ||
| 81 | +- 10 个并发 worker 是否能各自持有连接并完成 insert/select/update/select/delete/commit | ||
| 82 | + | ||
| 83 | +看到如下结尾表示通过: | ||
| 84 | + | ||
| 85 | +```text | ||
| 86 | +验证通过:Spring Boot 2.5.6 + HikariCP + MySQL Connector/J 可以通过 dolphin MySQL 协议访问 openGauss,并完成查询、增删改、事务、连接池上限和真实并发数据库操作验证。 | ||
| 87 | +``` | ||
| 88 | + | ||
| 89 | +## 连接链路与协议转换 | ||
| 90 | + | ||
| 91 | +本工程一次业务 SQL 的链路分为应用侧、驱动侧与服务端侧三段。 | ||
| 92 | + | ||
| 93 | +应用侧,Spring Boot 引入 `spring-boot-starter-jdbc` 后在类路径存在 HikariCP 时自动装配 `HikariDataSource`。业务通过 `DataSource.getConnection()` 获取连接,实际进入 `HikariPool.getConnection()`。HikariPool 用 `ConcurrentBag` 管理物理连接,存在空闲连接时复用,未达到 `maximumPoolSize` 时新建,达到上限后按 `connectionTimeout` 阻塞等待。新建物理连接时,HikariCP 调用 JDBC 驱动的 `connect` 方法。 | ||
| 94 | + | ||
| 95 | +驱动侧,MySQL Connector/J 的 `com.mysql.cj.jdbc.Driver.connect()` 建立到 `host:3306` 的 TCP 连接,按 MySQL 客户端服务端协议完成握手与认证。认证使用 B 库用户的 MySQL 原生密码,即通过 `set_native_password` 设置的密码,因此连接串需要 `allowPublicKeyRetrieval=true` 以支持公钥检索。连接建立后,HikariCP 会执行连接初始化探测,其中一项是查询事务隔离级别,例如执行 `SELECT @@session.transaction_isolation`。 | ||
| 96 | + | ||
| 97 | +服务端侧,openGauss 在 B 兼容库开启 dolphin 后,由 dolphin 插件在 `dolphin_server_port` 指定的端口(本工程设为 3306,需与 openGauss 自身 `port` 不同)监听 MySQL 协议。该监听的前置条件是 GUC 参数 `enable_dolphin_proto` 设为 on,且修改后需重启数据库生效。dolphin 通过抽象协议层接口,将收到的 MySQL 协议报文转换为 openGauss 可识别的逻辑执行,再把结果按 MySQL 协议格式封装返回。业务 SQL(`SELECT 1`、CRUD、事务提交)都经由同一条链路。 | ||
| 98 | + | ||
| 99 | +连接池只管理物理连接生命周期,真正的兼容边界在 MySQL 协议与 dolphin 的翻译层。只要 Connector/J 能与 dolphin 正常完成协议握手与系统变量探测,连接池层不会引入额外兼容问题。连接初始化阶段的隔离级别探测曾因早期 dolphin 返回 `default` 导致 HikariCP 建连失败,该问题在 openGauss 7.0.0-RC3(dolphin 5.2)已修复。更完整的链路与代码节点说明见 docs 仓《基于 HikariCP 开发》。 | ||
| 100 | + | ||
| 101 | +## 注意 | ||
| 102 | + | ||
| 103 | +openGauss 默认 `session_timeout` 通常为 10 分钟,因此本工程将 HikariCP `max-lifetime` 配为 540000 毫秒,小于服务端默认会话超时。 | ||
| 104 | + | ||
| 105 | +## 五类标准 SQL 操作验证 | ||
| 106 | + | ||
| 107 | +除上述 13 项 Spring Boot 集成验证外,工程还提供独立的五类标准 SQL 操作验证程序 `SqlCategoryVerificationRunner.java`,覆盖 DDL / DML / DQL / DCL / TCL 全部操作类别及并发连接测试,不依赖 Spring Boot 框架(纯 HikariCP + JDBC)。 | ||
| 108 | + | ||
| 109 | +### 验证分类 | ||
| 110 | + | ||
| 111 | +| 分类 | 核心关键字 | 验证项数 | | ||
| 112 | +|------|-----------|---------| | ||
| 113 | +| DDL | CREATE, ALTER, DROP, TRUNCATE, CTAS, INDEX | 7 | | ||
| 114 | +| DML | INSERT(单行+批量), UPDATE, DELETE, RETURNING | 5 | | ||
| 115 | +| DQL | SELECT, WHERE, ORDER BY, 聚合函数, GROUP BY, DISTINCT, 子查询 | 7 | | ||
| 116 | +| DCL | GRANT, REVOKE, 权限查询 | 4 | | ||
| 117 | +| TCL | COMMIT, ROLLBACK, SAVEPOINT, 隔离级别 | 4 | | ||
| 118 | +| 并发 | 10 worker 同时持连接执行完整事务 | 1 | | ||
| 119 | + | ||
| 120 | +### 运行方式 | ||
| 121 | + | ||
| 122 | +```powershell | ||
| 123 | +# 编译 | ||
| 124 | +mvn dependency:copy-dependencies -DoutputDirectory=target\lib -q | ||
| 125 | +javac -encoding UTF-8 -cp "target\lib\*" -d target\classes src\main\java\com\linyu\ospp\SqlCategoryVerificationRunner.java | ||
| 126 | + | ||
| 127 | +# 运行(需 JDK 11+) | ||
| 128 | +java -cp "target\classes;target\lib\*" com.linyu.ospp.SqlCategoryVerificationRunner | ||
| 129 | +``` | ||
| 130 | + | ||
| 131 | +### 自验结果 | ||
| 132 | + | ||
| 133 | +本次实测共 28 项验证,其中 26 项 PASS,2 项 INFO。INFO 项为 DML-05 getGeneratedKeys 与 TCL-04 隔离级别查询,两者均为只读能力探测,不计入 PASS。分类统计为 DDL 7 项、DML 4 项 PASS 加 1 项 INFO、DQL 7 项、DCL 4 项、TCL 3 项 PASS 加 1 项 INFO、并发 1 项。 | ||
| 134 | + | ||
| 135 | +实测输出见 `sql_category_verification.txt`,自验清单见 `五类SQL操作自验清单.txt`。 | ||
| @@ -0,0 +1,56 @@ | |||
| 1 | +# 大并发业务场景实测报告 | ||
| 2 | + | ||
| 3 | +> 对应代码:OpengaussCompatibility2hikari/src/main/java/com/linyu/ospp/HighConcurrencyBusinessDemo.java | ||
| 4 | +> 验证类:com.linyu.ospp.HighConcurrencyBusinessDemo | ||
| 5 | + | ||
| 6 | +## 一、环境与做法 | ||
| 7 | + | ||
| 8 | +本机 openEuler-20.03 WSL 内运行 openGauss 7.0.0-RC3 与 dolphin(MySQL 协议),服务已启动。Windows 侧经 172.18.15.52:3306/mysql_db 访问,数据库用户为 mysqluser/openGauss@123。 | ||
| 9 | + | ||
| 10 | +新增独立验证类 HighConcurrencyBusinessDemo.java,模拟多用户并发下单业务。每个 worker 在单个事务内完成扣减自己账户余额、写入订单与提交,比单纯建连或单条 CRUD 更贴近真实业务。 | ||
| 11 | + | ||
| 12 | +连接池 maximumPoolSize 固定为 10,故意小于并发数,用于制造连接池竞争。worker 数取 50、100、200 三档。 | ||
| 13 | + | ||
| 14 | +运行方式如下,后两个参数分别传入 100 与 200 即对应其余档位: | ||
| 15 | + | ||
| 16 | +```text | ||
| 17 | +java ...HighConcurrencyBusinessDemo 172.18.15.52 50 mysql_db mysqluser openGauss@123 | ||
| 18 | +``` | ||
| 19 | + | ||
| 20 | +## 二、连接池与并发设计 | ||
| 21 | + | ||
| 22 | +| 设计点 | 取值与行为 | | ||
| 23 | +| --- | --- | | ||
| 24 | +| 连接池上限 | maximumPoolSize=10,固定且小于所有并发档位 | | ||
| 25 | +| 每笔业务 | 单事务:UPDATE hc_accounts SET balance=balance-10,再 INSERT hc_orders,最后提交 | | ||
| 26 | +| 并发竞争 | worker 数远超 10,超出连接请求阻塞排队,验证上限不被突破 | | ||
| 27 | +| 一致性校验 | 订单数等于 worker 数,账户总余额等于初始 1000×N 减去扣减 10×N | | ||
| 28 | + | ||
| 29 | +## 三、实测结果 | ||
| 30 | + | ||
| 31 | +实跑环境为 openGauss 7.0.0-RC3 与 dolphin。 | ||
| 32 | + | ||
| 33 | +| 并发 worker | 峰值活跃连接 | 成功与失败 | 总耗时 | QPS | 账户总余额校验 | | ||
| 34 | +| --- | --- | --- | --- | --- | --- | | ||
| 35 | +| 50 | 8,上限 10 | 50 / 0 | 351 ms | 142.5 | 49500,通过 | | ||
| 36 | +| 100 | 10,上限 10 | 100 / 0 | 528 ms | 189.4 | 99000,通过 | | ||
| 37 | +| 200 | 10,上限 10 | 200 / 0 | 845 ms | 236.7 | 198000,通过 | | ||
| 38 | + | ||
| 39 | +## 四、一致性校验 | ||
| 40 | + | ||
| 41 | +控制台 [校验] 输出均为 PASS: | ||
| 42 | + | ||
| 43 | +```text | ||
| 44 | +[校验] 订单数=200 (期望=200) -> PASS | ||
| 45 | +[校验] 账户总余额=198000 (期望=198000) -> PASS | ||
| 46 | +``` | ||
| 47 | + | ||
| 48 | +订单数等于 worker 数,无丢失。账户总余额等于初始 1000×N 减去扣减 10×N,无超扣,无负余额。 | ||
| 49 | + | ||
| 50 | +## 五、结论 | ||
| 51 | + | ||
| 52 | +构建的是真实业务场景。每笔都是扣余额加写订单的事务,不是只建立连接。 | ||
| 53 | + | ||
| 54 | +超过最大连接数后再来连接,行为正确。200 并发争抢 10 个连接时,连接池被压满到 10 但从不突破上限,峰值活跃连接恒小于等于 maximumPoolSize,其余请求阻塞排队,全部成功提交,零失败。该结论与 verifyPoolLimit 一致,且压力更大。 | ||
| 55 | + | ||
| 56 | +高并发下数据一致。账户总余额与订单数完全符合预期,无并发写错乱。 | ||
| @@ -0,0 +1,57 @@ | |||
| 1 | +# HikariCP + MySQL JDBC 连接 openGauss B 兼容库 测试思维导图 | ||
| 2 | + | ||
| 3 | +> 本思维导图覆盖基于 HikariCP 连接池配合 MySQL JDBC Driver 连接 openGauss B 兼容模式(dolphin 插件)的兼容性测试范围。 | ||
| 4 | +> 可与 `自测报告_2026.md`(7.0.0-RC3 验证版)配合使用。 | ||
| 5 | + | ||
| 6 | +```mermaid | ||
| 7 | +mindmap | ||
| 8 | + root((HikariCP+MySQL JDBC<br/>连接 openGauss B库)) | ||
| 9 | + 环境准备 | ||
| 10 | + openGauss 安装与启动 | ||
| 11 | + B 兼容库创建 CREATE DATABASE ... DBCOMPATIBILITY='B' | ||
| 12 | + dolphin 插件加载 CREATE EXTENSION dolphin | ||
| 13 | + MySQL 协议端口开启(listen_addresses/port) | ||
| 14 | + 连接用户 MySQL 原生密码 set_native_password | ||
| 15 | + 客户端接入白名单(pg_hba.conf) | ||
| 16 | + 连接建立 | ||
| 17 | + JDBC URL 构造 jdbc:mysql://host:3306/db | ||
| 18 | + 驱动类 com.mysql.cj.jdbc.Driver | ||
| 19 | + 关键参数 useSSL/serverTimezone/allowPublicKeyRetrieval | ||
| 20 | + HikariConfig 基本配置 | ||
| 21 | + 连接池启动与数据源获取 | ||
| 22 | + 事务隔离级别探测(SELECT @@session.transaction_isolation) | ||
| 23 | + 连接池参数 | ||
| 24 | + maximumPoolSize | ||
| 25 | + minimumIdle | ||
| 26 | + connectionTimeout | ||
| 27 | + idleTimeout | ||
| 28 | + maxLifetime | ||
| 29 | + keepaliveTime | ||
| 30 | + connectionTestQuery | ||
| 31 | + 基础 CRUD | ||
| 32 | + DDL 建表/删表 | ||
| 33 | + INSERT/UPDATE/DELETE | ||
| 34 | + SELECT 单表/多条件 | ||
| 35 | + 事务提交与回滚 | ||
| 36 | + 高级特性 | ||
| 37 | + 存储过程调用 | ||
| 38 | + 多结果集处理 | ||
| 39 | + Dolphin 语法兼容 | ||
| 40 | + TPCH/TPCDS 正确性 | ||
| 41 | + 并发与稳定性 | ||
| 42 | + 高并发获取连接 | ||
| 43 | + 连接泄漏检测 | ||
| 44 | + 长时间压测内存与连接释放 | ||
| 45 | + maxLifetime 到期重连 | ||
| 46 | + 兼容性与异常 | ||
| 47 | + MySQL 驱动版本矩阵(8.0.x / 8.4.x) | ||
| 48 | + openGauss 版本矩阵(5.x / 6.0-RC1 / 7.0-RC3) | ||
| 49 | + 已知缺陷追踪 | ||
| 50 | + 2024:事务隔离级别 'default' 无法映射 | ||
| 51 | + 2026:7.0.0-RC3 已可正常建连 | ||
| 52 | + 异常用例与规避措施 | ||
| 53 | + 自测与交付 | ||
| 54 | + 自测报告(本仓库 自测报告_2026.md) | ||
| 55 | + 指导文档(docs 仓 hikaricp_development.md) | ||
| 56 | + 示例代码(HikariMySQLVerify.java) | ||
| 57 | +``` | ||
| @@ -0,0 +1,64 @@ | |||
| 1 | +# 自测报告 | ||
| 2 | + | ||
| 3 | +> 验证类:com.linyu.ospp.HikariVerificationRunner | ||
| 4 | +> 数据库:openGauss 7.0.0-RC3(B 兼容库 + dolphin) | ||
| 5 | +> 驱动:MySQL Connector/J 8.0.20 + HikariCP 5.1.0 | ||
| 6 | + | ||
| 7 | +## 一、基本信息 | ||
| 8 | + | ||
| 9 | +关键词:HikariCP;MySQL JDBC;openGauss B 兼容库;dolphin;连接池 | ||
| 10 | + | ||
| 11 | +摘要:在 openGauss 7.0.0-RC3 的 B 兼容库与 dolphin 环境下,使用 HikariCP 5.1.0 配合 MySQL Connector/J 8.0.20 验证通过 MySQL 协议连接 openGauss 的可行性,覆盖建连、基础 SQL、CRUD 与连接池参数生效等场景。 | ||
| 12 | + | ||
| 13 | +## 二、环境与准备 | ||
| 14 | + | ||
| 15 | +- 数据库:openGauss 7.0.0-RC3(B 兼容库,dolphin 插件) | ||
| 16 | +- JDBC 驱动:MySQL Connector/J 8.0.20 | ||
| 17 | +- 连接池:HikariCP 5.1.0 | ||
| 18 | +- 连接串:`jdbc:mysql://<host>:3306/<db>?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true` | ||
| 19 | +- 用户:需通过 `set_native_password` 设置 MySQL 原生密码,否则 Connector/J 在公钥检索阶段无法认证。 | ||
| 20 | + | ||
| 21 | +## 三、验证结果 | ||
| 22 | + | ||
| 23 | +### 3.1 连接与基础能力 | ||
| 24 | + | ||
| 25 | +- 连接池初始化正常,`HikariPool` 成功建立物理连接。 | ||
| 26 | +- `Connection.isValid` 返回 true。 | ||
| 27 | +- `SELECT 1` 返回 1。 | ||
| 28 | +- `SELECT version()` 返回 `(openGauss 7.0.0-RC3 build f08516a2)`,确认服务端为 openGauss 而非 MySQL。 | ||
| 29 | +- 版本探测、系统变量查询均正常完成。 | ||
| 30 | + | ||
| 31 | +### 3.2 CRUD | ||
| 32 | + | ||
| 33 | +- INSERT:成功写入业务表记录。 | ||
| 34 | +- SELECT:可查询写入结果与基础数据。 | ||
| 35 | +- UPDATE:成功更新记录字段。 | ||
| 36 | +- DELETE:成功删除记录。 | ||
| 37 | + | ||
| 38 | +### 3.3 事务 | ||
| 39 | + | ||
| 40 | +- 使用显式事务完成提交,提交后数据可查询。 | ||
| 41 | +- 隔离级别探测正常,未出现无法映射 `default` 的建连失败。 | ||
| 42 | + | ||
| 43 | +### 3.4 连接池参数 | ||
| 44 | + | ||
| 45 | +- `maximumPoolSize`、`connectionTimeout`、`maxLifetime`、`keepaliveTime` 等参数按配置生效。 | ||
| 46 | +- 借满 `maximumPoolSize` 后,再申请连接进入等待,不突破上限;释放后等待请求恢复执行。 | ||
| 47 | + | ||
| 48 | +### 3.5 并发 | ||
| 49 | + | ||
| 50 | +- 10 个 worker 各自持有连接完成 insert/select/update/select/delete/commit,全部成功。 | ||
| 51 | + | ||
| 52 | +## 四、历史兼容性说明 | ||
| 53 | + | ||
| 54 | +开源之夏 2024 报告曾记录 MySQL 驱动经 HikariCP 无法建连,根因是早期 dolphin 版本对 `SELECT @@session.transaction_isolation` 返回字面量 `default`(更早版本报 `missing FROM-clause entry for table "session"`),HikariCP 探测隔离级别时无法映射而建连失败。版本边界为 6.0.0-RC1(dolphin)可复现,7.0.0-RC3(dolphin 5.2)已不复现。本次在 7.0.0-RC3 下实跑,连接与事务均正常。 | ||
| 55 | + | ||
| 56 | +## 五、结论 | ||
| 57 | + | ||
| 58 | +在 openGauss 7.0.0-RC3(B 兼容库 + dolphin)环境下,HikariCP 5.1.0 配合 MySQL Connector/J 8.0.20 可经 MySQL 协议正常连接 openGauss,并完成建连、CRUD、事务、连接池参数生效与并发操作验证。 | ||
| 59 | + | ||
| 60 | +## 六、参考资料 | ||
| 61 | + | ||
| 62 | +- openGauss B 兼容库与 dolphin 插件说明 | ||
| 63 | +- HikariCP 官方文档 | ||
| 64 | +- MySQL Connector/J 8.0 文档 | ||
| @@ -0,0 +1,173 @@ | |||
| 1 | +========================================================== | ||
| 2 | +openGauss + dolphin MySQL 协议 配置详细清单 | ||
| 3 | +========================================================== | ||
| 4 | +生成时间 : 2026-08-15 | ||
| 5 | +环境 : openEuler 20.03 (WSL), 主机名 liyang, 用户 cyh | ||
| 6 | +openGauss: 7.0.0 build 93bfec54 (dolphin 功能等价于 7.0.0-RC3) | ||
| 7 | +数据目录 : /home/cyh/openGauss/node1 | ||
| 8 | +安装目录 : /home/cyh/openGauss | ||
| 9 | + | ||
| 10 | +---------------------------------------------------------- | ||
| 11 | +一、服务与监听端口 | ||
| 12 | +---------------------------------------------------------- | ||
| 13 | +[1] openGauss 服务状态: 运行中 (gaussdb) | ||
| 14 | + 启动命令: gs_ctl start -D /home/cyh/openGauss/node1 -Z single_node | ||
| 15 | +[2] PG 原生协议监听 : 127.0.0.1:5432 | ||
| 16 | +[3] MySQL 协议监听 : 127.0.0.1:3306 (dolphin, 启用) | ||
| 17 | +[4] MySQL 握手信息 : 服务器版本 8.0.28-dolphin-server | ||
| 18 | + 协议版本 10, 默认认证插件 caching_sha2_password | ||
| 19 | + | ||
| 20 | +---------------------------------------------------------- | ||
| 21 | +二、postgresql.conf 关键配置 | ||
| 22 | +---------------------------------------------------------- | ||
| 23 | +配置文件: /home/cyh/openGauss/node1/postgresql.conf | ||
| 24 | +[1] listen_addresses = '127.0.0.1' # 第68行 | ||
| 25 | +[2] port = 5432 # 默认值, 未修改 | ||
| 26 | +[3] enable_dolphin_proto = on # 第919行, 打开 MySQL 协议 | ||
| 27 | +[4] dolphin_server_port = 3306 # 第920行, MySQL 协议端口 | ||
| 28 | +[5] shared_preload_libraries = security_plugin | ||
| 29 | +[6] 备份文件: postgresql.conf.bak_dolphin (修改前备份) | ||
| 30 | + | ||
| 31 | +---------------------------------------------------------- | ||
| 32 | +三、pg_hba.conf 认证配置 | ||
| 33 | +---------------------------------------------------------- | ||
| 34 | +配置文件: /home/cyh/openGauss/node1/pg_hba.conf | ||
| 35 | + local all all 127.0.0.1/32 trust | ||
| 36 | + host all mysql_test_db 127.0.0.1/32 sha256 # 新增(第90行) | ||
| 37 | + host all all 127.0.0.1/32 trust | ||
| 38 | + host all all ::1/128 trust | ||
| 39 | +说明: | ||
| 40 | + - PG 5432 端口实际走 trust (127.0.0.1 规则在前) | ||
| 41 | + - MySQL 3306 端口认证由 dolphin 自身处理, 不受 pg_hba 影响 | ||
| 42 | + | ||
| 43 | +---------------------------------------------------------- | ||
| 44 | +四、数据库清单 | ||
| 45 | +---------------------------------------------------------- | ||
| 46 | + postgres : compat=A, owner=cyh (管理库) | ||
| 47 | + bbb : compat=B, owner=cyh [基座库, 实际数据所在地] | ||
| 48 | + ly_dbbtest : compat=B, owner=cyh (之前建的 B 兼容库) | ||
| 49 | + proto_test_db: compat=B, owner=cyh (原计划残留, 未使用, 可删) | ||
| 50 | + | ||
| 51 | +关键说明: | ||
| 52 | + dolphin.default_database_name = bbb | ||
| 53 | + (boot_val=bbb, 编译内置默认, context=sighup) | ||
| 54 | + -> 所有 MySQL 协议连接固定落到 bbb 库; | ||
| 55 | + 客户端 URL 库名映射为 bbb 库内的 schema。 | ||
| 56 | + | ||
| 57 | +---------------------------------------------------------- | ||
| 58 | +五、数据对象 (dolphin 实际生效位置: bbb 库) | ||
| 59 | +---------------------------------------------------------- | ||
| 60 | +[1] schema mysql_test_db owner = mysql_test_db | ||
| 61 | +[2] 表结构 (bbb.mysql_test_db.user): | ||
| 62 | + id : integer, AUTO_INCREMENT, PRIMARY KEY (user_pkey) | ||
| 63 | + name : varchar(50), NOT NULL, charset=utf8mb4 | ||
| 64 | + age : integer | ||
| 65 | +[3] 表 owner = cyh (授权 mysql_test_db 全 DML arwdDxt) | ||
| 66 | +[4] 初始数据: | ||
| 67 | + (1, 张三, 18) | ||
| 68 | + (2, 李四, 19) | ||
| 69 | + (3, 王五, 20) | ||
| 70 | +[5] 授权语句: | ||
| 71 | + GRANT USAGE ON SCHEMA mysql_test_db TO mysql_test_db; | ||
| 72 | + GRANT ALL ON ALL TABLES IN SCHEMA mysql_test_db TO mysql_test_db; | ||
| 73 | + GRANT ALL ON ALL SEQUENCES IN SCHEMA mysql_test_db TO mysql_test_db; | ||
| 74 | + | ||
| 75 | +---------------------------------------------------------- | ||
| 76 | +六、用户与认证 | ||
| 77 | +---------------------------------------------------------- | ||
| 78 | +[1] 角色: mysql_test_db | ||
| 79 | + login=true, super=false, createrole=false, createdb=false | ||
| 80 | +[2] 密码: xxxxxx | ||
| 81 | +[3] 已执行 set_native_password('mysql_test_db','xxxxxx','') | ||
| 82 | + 生成 native 密码哈希 | ||
| 83 | +[4] MySQL 协议认证实测: caching_sha2_password 直连通过 | ||
| 84 | + | ||
| 85 | +---------------------------------------------------------- | ||
| 86 | +七、dolphin 相关 GUC (会话级) | ||
| 87 | +---------------------------------------------------------- | ||
| 88 | + dolphin.b_compatibility_mode = on | ||
| 89 | + dolphin.cmpt_version = 5.7 | ||
| 90 | + dolphin.default_database_name = bbb | ||
| 91 | + dolphin.default_week_format = 0 | ||
| 92 | + dolphin.div_precision_increment = 4 | ||
| 93 | + dolphin.enable_procedure_executestmt = off | ||
| 94 | + dolphin.lc_time_names = en_US | ||
| 95 | + dolphin.lower_case_table_names = 0 | ||
| 96 | + dolphin.mysql_ca = cacert.pem | ||
| 97 | + dolphin.mysql_server_cert = server.crt | ||
| 98 | + dolphin.mysql_server_key = server.key | ||
| 99 | + dolphin.nulls_minimal_policy = on | ||
| 100 | + dolphin.optimizer_switch = default | ||
| 101 | + dolphin.sql_mode = sql_mode_strict,sql_mode_full_group,no_zero_date,block_return_multi_results,error_for_division_by_zero,escape_quotes,disable_escape_bytea | ||
| 102 | + dolphin.support_interval_to = off | ||
| 103 | + dolphin.treat_float_with_precision_as_float_type = off | ||
| 104 | + dolphin.use_const_value_as_colname = on | ||
| 105 | + dolphin.bit_output = bin | ||
| 106 | + dolphin.b_db_timestamp = 0 | ||
| 107 | + | ||
| 108 | +---------------------------------------------------------- | ||
| 109 | +八、Spring Boot 侧配置 (application.yml / 环境变量) | ||
| 110 | +---------------------------------------------------------- | ||
| 111 | +[1] JDBC URL: | ||
| 112 | + jdbc:mysql://127.0.0.1:3306/mysql_test_db | ||
| 113 | + ?useSSL=false&serverTimezone=UTC&characterEncoding=utf-8&allowPublicKeyRetrieval=true | ||
| 114 | +[2] 用户名: mysql_test_db | ||
| 115 | +[3] 密码 : xxxxxx | ||
| 116 | +[4] 驱动 : com.mysql.cj.jdbc.Driver (MySQL Connector/J 8.0.20) | ||
| 117 | +[5] HikariCP: | ||
| 118 | + pool-name=HikariCP-openGauss-OSPP | ||
| 119 | + maximum-pool-size=10 | ||
| 120 | + minimum-idle=2 | ||
| 121 | + connection-timeout=30000 | ||
| 122 | + idle-timeout=300000 | ||
| 123 | + max-lifetime=540000 | ||
| 124 | + keepalive-time=300000 | ||
| 125 | +[6] 启动命令 (PowerShell): | ||
| 126 | + $env:OSPP_VERIFY_ENABLED="true" | ||
| 127 | + $env:OPENGAUSS_MYSQL_URL="jdbc:mysql://127.0.0.1:3306/mysql_test_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf-8&allowPublicKeyRetrieval=true" | ||
| 128 | + $env:OPENGAUSS_MYSQL_USERNAME="mysql_test_db" | ||
| 129 | + $env:OPENGAUSS_MYSQL_PASSWORD="xxxxxx" | ||
| 130 | + mvn spring-boot:run | ||
| 131 | + | ||
| 132 | +---------------------------------------------------------- | ||
| 133 | +九、验证结果 (Spring Boot 全自动验证, 退出码 0) | ||
| 134 | +---------------------------------------------------------- | ||
| 135 | + 1. HikariDataSource 自动装配通过 (HikariCP 5.1.0) | ||
| 136 | + 2. JDBC 连接有效性 true, SELECT 1 = 1 | ||
| 137 | + 3. dolphin 插件安装数量 1, enable_dolphin_proto=on, port=3306 | ||
| 138 | + 4. DATABASE()=mysql_test_db, current_schema()=mysql_test_db (映射正确) | ||
| 139 | + 5. 历史临时数据清理 0 行 | ||
| 140 | + 6. user 表查询 3 行: 张三/李四/王五 | ||
| 141 | + 7. INSERT 成功 (影响 1 行) | ||
| 142 | + 8. UPDATE 成功 (影响 1 行) | ||
| 143 | + 9. Spring 事务提交成功 | ||
| 144 | + 10. 连接池借满 10 条, 第 11 条等待, 释放后恢复 (上限校验通过) | ||
| 145 | + 11. 10 worker 真实并发 insert/select/update/select/delete/commit 全过 | ||
| 146 | + 12. DELETE + 最终清理, 恢复为 3 条初始数据 | ||
| 147 | + 13. 结论: 全链路验证通过 | ||
| 148 | + | ||
| 149 | +---------------------------------------------------------- | ||
| 150 | +十、注意事项与待办 | ||
| 151 | +---------------------------------------------------------- | ||
| 152 | +[1] 不要删除 bbb 库 —— 它是 dolphin 基座库 | ||
| 153 | +[2] 残留可清理: | ||
| 154 | + DROP DATABASE proto_test_db; # 原计划残留, 未使用 | ||
| 155 | +[3] 可选调整: | ||
| 156 | + ALTER TABLE mysql_test_db.user OWNER TO mysql_test_db; | ||
| 157 | + # 若将来 Hibernate ddl-auto=create/drop 需重建表, 建议执行 | ||
| 158 | +[4] WSL1 环境 127.0.0.1 直连可用 (bashrc 有 sync_file_range shim) | ||
| 159 | +[5] 若改基座库 (需重启): | ||
| 160 | + gs_guc set -D /home/cyh/openGauss/node1 -c "dolphin.default_database_name='xxx'" | ||
| 161 | + gs_ctl restart -D /home/cyh/openGauss/node1 -Z single_node | ||
| 162 | +[6] 常用操作命令: | ||
| 163 | + source /home/cyh/openGauss/env.sh 2>/dev/null || true | ||
| 164 | + export GAUSSHOME=/home/cyh/openGauss | ||
| 165 | + export PATH=$GAUSSHOME/bin:$PATH | ||
| 166 | + export LD_LIBRARY_PATH=$GAUSSHOME/lib:$LD_LIBRARY_PATH | ||
| 167 | + gsql -d bbb -p 5432 -r # 连库 | ||
| 168 | + gs_ctl restart -D /home/cyh/openGauss/node1 -Z single_node | ||
| 169 | + ss -ltnp | grep -E ':(5432|3306)' # 查监听 | ||
| 170 | + | ||
| 171 | +========================================================== | ||
| 172 | +清单结束 | ||
| 173 | +========================================================== | ||
| @@ -4,6 +4,13 @@ | |||
| 4 | xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | 4 | xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
| 5 | <modelVersion>4.0.0</modelVersion> | 5 | <modelVersion>4.0.0</modelVersion> |
| 6 | 6 | ||
| 7 | + <parent> | ||
| 8 | + <groupId>org.springframework.boot</groupId> | ||
| 9 | + <artifactId>spring-boot-starter-parent</artifactId> | ||
| 10 | + <version>2.5.6</version> | ||
| 11 | + <relativePath/> | ||
| 12 | + </parent> | ||
| 13 | + | ||
| 7 | <groupId>org.hikaricptest</groupId> | 14 | <groupId>org.hikaricptest</groupId> |
| 8 | <artifactId>test_hikari</artifactId> | 15 | <artifactId>test_hikari</artifactId> |
| 9 | <version>1.0-SNAPSHOT</version> | 16 | <version>1.0-SNAPSHOT</version> |
| @@ -16,43 +23,34 @@ | |||
| 16 | </developers> | 23 | </developers> |
| 17 | 24 | ||
| 18 | <properties> | 25 | <properties> |
| 19 | - <maven.compiler.source>11</maven.compiler.source> | 26 | + <java.version>11</java.version> |
| 20 | - <maven.compiler.target>11</maven.compiler.target> | ||
| 21 | <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | 27 | <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
| 28 | + <!-- 显式覆盖 Spring Boot 2.5.6 默认 HikariCP 4.x,使用 5.x --> | ||
| 29 | + <hikaricp.version>5.1.0</hikaricp.version> | ||
| 22 | </properties> | 30 | </properties> |
| 23 | 31 | ||
| 24 | <dependencies> | 32 | <dependencies> |
| 25 | - <!-- log --> | 33 | + <!-- Spring Boot JDBC(自动装配 HikariDataSource + JdbcTemplate + 事务) --> |
| 26 | <dependency> | 34 | <dependency> |
| 27 | - <groupId>org.slf4j</groupId> | 35 | + <groupId>org.springframework.boot</groupId> |
| 28 | - <artifactId>slf4j-api</artifactId> | 36 | + <artifactId>spring-boot-starter-jdbc</artifactId> |
| 29 | - <version>1.7.28</version> | ||
| 30 | - <type>jar</type> | ||
| 31 | - <scope>compile</scope> | ||
| 32 | </dependency> | 37 | </dependency> |
| 33 | - <dependency> | 38 | + <!-- MySQL JDBC 驱动 --> |
| 34 | - <groupId>ch.qos.logback</groupId> | ||
| 35 | - <artifactId>logback-core</artifactId> | ||
| 36 | - <version>1.2.3</version> | ||
| 37 | - <type>jar</type> | ||
| 38 | - </dependency> | ||
| 39 | - <dependency> | ||
| 40 | - <groupId>ch.qos.logback</groupId> | ||
| 41 | - <artifactId>logback-classic</artifactId> | ||
| 42 | - <version>1.2.3</version> | ||
| 43 | - <type>jar</type> | ||
| 44 | - </dependency> | ||
| 45 | - <!-- hikari --> | ||
| 46 | <dependency> | 39 | <dependency> |
| 47 | <groupId>mysql</groupId> | 40 | <groupId>mysql</groupId> |
| 48 | <artifactId>mysql-connector-java</artifactId> | 41 | <artifactId>mysql-connector-java</artifactId> |
| 49 | <version>8.0.20</version> | 42 | <version>8.0.20</version> |
| 50 | </dependency> | 43 | </dependency> |
| 51 | - <!-- hikari --> | 44 | + <!-- HikariCP(版本经 hikaricp.version 覆盖为 5.1.0) --> |
| 52 | <dependency> | 45 | <dependency> |
| 53 | <groupId>com.zaxxer</groupId> | 46 | <groupId>com.zaxxer</groupId> |
| 54 | <artifactId>HikariCP</artifactId> | 47 | <artifactId>HikariCP</artifactId> |
| 55 | - <version>5.0.1</version> | 48 | + <version>${hikaricp.version}</version> |
| 49 | + </dependency> | ||
| 50 | + <!-- log --> | ||
| 51 | + <dependency> | ||
| 52 | + <groupId>org.slf4j</groupId> | ||
| 53 | + <artifactId>slf4j-api</artifactId> | ||
| 56 | </dependency> | 54 | </dependency> |
| 57 | <!-- junit --> | 55 | <!-- junit --> |
| 58 | <dependency> | 56 | <dependency> |
| @@ -61,6 +59,12 @@ | |||
| 61 | <version>4.12</version> | 59 | <version>4.12</version> |
| 62 | <scope>compile</scope> | 60 | <scope>compile</scope> |
| 63 | </dependency> | 61 | </dependency> |
| 62 | + <!-- Spring Boot test(测试类使用 JUnit5 + @SpringBootTest) --> | ||
| 63 | + <dependency> | ||
| 64 | + <groupId>org.springframework.boot</groupId> | ||
| 65 | + <artifactId>spring-boot-starter-test</artifactId> | ||
| 66 | + <scope>test</scope> | ||
| 67 | + </dependency> | ||
| 64 | <!-- PG驱动 --> | 68 | <!-- PG驱动 --> |
| 65 | <dependency> | 69 | <dependency> |
| 66 | <groupId>org.opengauss</groupId> | 70 | <groupId>org.opengauss</groupId> |
| @@ -82,4 +86,16 @@ | |||
| 82 | </dependency> | 86 | </dependency> |
| 83 | </dependencies> | 87 | </dependencies> |
| 84 | 88 | ||
| 85 | -</project> | 89 | + <build> |
| 90 | + <plugins> | ||
| 91 | + <plugin> | ||
| 92 | + <groupId>org.springframework.boot</groupId> | ||
| 93 | + <artifactId>spring-boot-maven-plugin</artifactId> | ||
| 94 | + <configuration> | ||
| 95 | + <mainClass>com.linyu.ospp.SpringBootOsppApplication</mainClass> | ||
| 96 | + </configuration> | ||
| 97 | + </plugin> | ||
| 98 | + </plugins> | ||
| 99 | + </build> | ||
| 100 | + | ||
| 101 | +</project> | ||
| @@ -0,0 +1,120 @@ | |||
| 1 | + | ||
| 2 | + . ____ _ __ _ _ | ||
| 3 | + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ | ||
| 4 | +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ | ||
| 5 | + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) | ||
| 6 | + ' |____| .__|_| |_|_| |_\__, | / / / / | ||
| 7 | + =========|_|==============|___/=/_/_/_/ | ||
| 8 | + :: Spring Boot :: (v2.5.6) | ||
| 9 | + | ||
| 10 | +2026-08-27 02:03:26.239 INFO 41420 --- [ main] c.linyu.ospp.SpringBootOsppApplication : Starting SpringBootOsppApplication using Java 17.0.19 on cuicui_dada with PID 41420 (C:\Users\cuicu\Desktop\ascend ir\opengauss\repos\examples\OpengaussCompatibility2hikari\target\classes started by cuicu in C:\Users\cuicu\Desktop\ascend ir\opengauss\repos\examples\OpengaussCompatibility2hikari) | ||
| 11 | +2026-08-27 02:03:26.241 INFO 41420 --- [ main] c.linyu.ospp.SpringBootOsppApplication : No active profile set, falling back to default profiles: default | ||
| 12 | +2026-08-27 02:03:26.752 INFO 41420 --- [ main] c.linyu.ospp.SpringBootOsppApplication : Started SpringBootOsppApplication in 0.942 seconds (JVM running for 1.379) | ||
| 13 | + | ||
| 14 | +==================== 1. 验证 HikariCP 数据源自动装配 ==================== | ||
| 15 | +测试目的:确认 Spring Boot 2.5.6 通过 spring-boot-starter-jdbc 自动创建 HikariDataSource,并读取到预期连接池参数。 | ||
| 16 | +数据源实现类:com.zaxxer.hikari.HikariDataSource | ||
| 17 | +HikariCP 运行版本:5.1.0 | ||
| 18 | +HikariCP 加载位置:file:/C:/Users/cuicu/.m2/repository/com/zaxxer/HikariCP/5.1.0/HikariCP-5.1.0.jar | ||
| 19 | +连接池名称:HikariCP-openGauss-OSPP | ||
| 20 | +最大连接数 maximumPoolSize:10 | ||
| 21 | +最小空闲连接数 minimumIdle:2 | ||
| 22 | +获取连接超时时间 connectionTimeout(ms):30000 | ||
| 23 | +空闲连接保留时间 idleTimeout(ms):300000 | ||
| 24 | +连接最大生命周期 maxLifetime(ms):540000 | ||
| 25 | +连接保活时间 keepaliveTime(ms):300000 | ||
| 26 | +数据源装配校验通过:当前使用 HikariCP,连接池参数已生效。 | ||
| 27 | + | ||
| 28 | +==================== 2. 验证数据库连接与基础 SQL ==================== | ||
| 29 | +测试目的:确认 MySQL Connector/J 可以通过 dolphin MySQL 协议端口连接 openGauss,并执行基础查询。 | ||
| 30 | +2026-08-27 02:03:26.757 INFO 41420 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariCP-openGauss-OSPP - Starting... | ||
| 31 | +2026-08-27 02:03:26.879 INFO 41420 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariCP-openGauss-OSPP - Added connection com.mysql.cj.jdbc.ConnectionImpl@7c4fc2bf | ||
| 32 | +2026-08-27 02:03:26.881 INFO 41420 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariCP-openGauss-OSPP - Start completed. | ||
| 33 | +JDBC 连接有效性校验结果:true | ||
| 34 | +基础 SQL 执行结果:SELECT 1 = 1 | ||
| 35 | +数据库版本信息:(openGauss 7.0.0 build 93bfec54) compiled at 2026-08-26 10:21:31 commit 1000 last mr 9389 release on x86_64-pc-linux-gnu, compiled by g++ (GCC) 10.3.0, 64-bit | ||
| 36 | +运行环境:Windows 11 + WSL2 openEuler 24.03 | 主机: cuicuidada | Java 17.0.19 | 运行日期: 2026-08-27 | ||
| 37 | +服务器版本:(openGauss 7.0.0 build 93bfec54) compiled at 2026-08-26 10:21:31 commit 1000 last mr 9389 | ||
| 38 | +数据库连接与基础 SQL 校验通过。 | ||
| 39 | + | ||
| 40 | +==================== 3. 验证 dolphin 插件与 MySQL 协议配置 ==================== | ||
| 41 | +测试目的:直接查询 openGauss 系统表,确认 dolphin 插件已经安装,并确认 dolphin MySQL 协议开关和监听端口配置存在。 | ||
| 42 | +dolphin 插件安装数量:1 | ||
| 43 | +enable_dolphin_proto 配置值:on | ||
| 44 | +dolphin_server_port 配置值:3306 | ||
| 45 | +dolphin 插件与 MySQL 协议配置校验通过。 | ||
| 46 | + | ||
| 47 | +==================== 4. 验证 dolphin 客户端库名与 schema 映射 ==================== | ||
| 48 | +测试目的:确认 JDBC URL 中的 mysql_test_db 能映射到 openGauss B 兼容库中的 mysql_test_db schema,避免文档只写库名但实际落错 schema。 | ||
| 49 | +JDBC URL 客户端库名 DATABASE():mysql_test_db | ||
| 50 | +openGauss 当前 schema current_schema():mysql_test_db | ||
| 51 | +dolphin 库名与 schema 映射校验通过。 | ||
| 52 | + | ||
| 53 | +==================== 5. 清理历史验证数据 ==================== | ||
| 54 | +测试目的:删除上一次验证遗留的临时数据,保证本次 CRUD、事务、并发验证结果可重复。 | ||
| 55 | +历史临时数据清理结果:固定名称数据 0 行,并发名称数据 0 行。 | ||
| 56 | + | ||
| 57 | +==================== 6. 验证初始业务表查询 ==================== | ||
| 58 | +测试目的:确认文档准备步骤中的 user 表可以通过 MySQL 协议查询,并且至少包含张三、李四、王五三条基础数据。 | ||
| 59 | +初始查询行数:3 | ||
| 60 | + 用户记录:id=1,name=张三,age=18 | ||
| 61 | + 用户记录:id=2,name=李四,age=19 | ||
| 62 | + 用户记录:id=3,name=王五,age=20 | ||
| 63 | +初始业务表校验通过:已查到张三、李四、王五三条基础数据。 | ||
| 64 | + | ||
| 65 | +==================== 7. 验证 INSERT 插入能力 ==================== | ||
| 66 | +测试目的:向 user 表插入一条临时数据,确认 MySQL 协议下的写入链路可用。 | ||
| 67 | +INSERT 执行影响行数:1 | ||
| 68 | +插入后行数:4 | ||
| 69 | + 用户记录:id=1,name=张三,age=18 | ||
| 70 | + 用户记录:id=2,name=李四,age=19 | ||
| 71 | + 用户记录:id=3,name=王五,age=20 | ||
| 72 | + 用户记录:id=17,name=zhaoliu,age=18 | ||
| 73 | +插入后查询校验通过:基础数据仍然存在。 | ||
| 74 | + | ||
| 75 | +==================== 8. 验证 UPDATE 更新能力 ==================== | ||
| 76 | +测试目的:按 name 找到刚插入的数据并更新,确认 MySQL 协议下的更新链路可用。 | ||
| 77 | +UPDATE 执行影响行数:1 | ||
| 78 | +用户数据校验通过:name=zhaoliuliuliu,age=28。 | ||
| 79 | +更新后行数:4 | ||
| 80 | + 用户记录:id=1,name=张三,age=18 | ||
| 81 | + 用户记录:id=2,name=李四,age=19 | ||
| 82 | + 用户记录:id=3,name=王五,age=20 | ||
| 83 | + 用户记录:id=17,name=zhaoliuliuliu,age=28 | ||
| 84 | +更新后查询校验通过:基础数据仍然存在。 | ||
| 85 | + | ||
| 86 | +==================== 9. 验证 Spring 事务提交 ==================== | ||
| 87 | +测试目的:通过 TransactionTemplate 插入并提交一条数据,确认 Spring 事务管理器和 Hikari 连接协同正常。 | ||
| 88 | +事务提交校验通过:spring-transaction 数据已提交并可查询。 | ||
| 89 | + | ||
| 90 | +==================== 10. 验证连接数超出 Hikari 配置后的等待行为 ==================== | ||
| 91 | +测试目的:先借满 maximumPoolSize 条连接,再申请第 maximumPoolSize + 1 条连接,确认 HikariCP 不会突破最大连接数,而是等待空闲连接。 | ||
| 92 | +连接池借满后活跃连接数:10 | ||
| 93 | +连接池借满后空闲连接数:0 | ||
| 94 | +连接池借满后等待线程数:0 | ||
| 95 | +已借出连接数:10,配置最大连接数:10 | ||
| 96 | +第 11 条连接观察窗口:1000 ms,连接池 connectionTimeout=30000 ms | ||
| 97 | +申请第 11 条连接时等待线程数:1 | ||
| 98 | +连接池上限等待校验通过:第 11 条连接没有突破最大连接数,而是在等待空闲连接。 | ||
| 99 | +已释放 1 条占用连接,用于确认等待中的连接请求可以恢复执行。 | ||
| 100 | +用户数据校验通过:name=pool-limit-user,age=33。 | ||
| 101 | +连接池上限恢复校验通过:释放 1 条连接后,等待中的请求可以继续获取连接并执行 SQL。 | ||
| 102 | + | ||
| 103 | +==================== 11. 验证并发真实数据库操作 ==================== | ||
| 104 | +测试目的:启动 10 个 worker,每个 worker 持有自己的连接,并完成 insert/select/update/select/delete/commit,证明不是只拿到连接就算并发通过。 | ||
| 105 | +并发真实数据库操作校验通过:10/10 个 worker 均完成 insert/select/update/select/delete/commit。 | ||
| 106 | + | ||
| 107 | +==================== 12. 验证 DELETE 删除能力与最终清理 ==================== | ||
| 108 | +测试目的:删除本次单线程 CRUD 临时数据,并清理事务、并发、连接数验证产生的临时数据。 | ||
| 109 | +DELETE 执行影响行数:1 | ||
| 110 | +历史临时数据清理结果:固定名称数据 2 行,并发名称数据 0 行。 | ||
| 111 | +最终清理后行数:3 | ||
| 112 | + 用户记录:id=1,name=张三,age=18 | ||
| 113 | + 用户记录:id=2,name=李四,age=19 | ||
| 114 | + 用户记录:id=3,name=王五,age=20 | ||
| 115 | +最终清理后查询校验通过:基础数据仍然存在。 | ||
| 116 | + | ||
| 117 | +==================== 13. 验证结论 ==================== | ||
| 118 | +验证通过:Spring Boot 2.5.6 + HikariCP + MySQL Connector/J 可以通过 dolphin MySQL 协议访问 openGauss,并完成查询、增删改、事务、连接池上限和真实并发数据库操作验证。 | ||
| 119 | +2026-08-27 02:03:28.408 INFO 41420 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariCP-openGauss-OSPP - Shutdown initiated... | ||
| 120 | +2026-08-27 02:03:28.415 INFO 41420 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariCP-openGauss-OSPP - Shutdown completed. | ||
| @@ -0,0 +1,245 @@ | |||
| 1 | +======================================== | ||
| 2 | + openGauss B 兼容模式 五类 SQL 操作验证 | ||
| 3 | + 驱动:MySQL Connector/J 8.0.20 | ||
| 4 | + 连接池:HikariCP | ||
| 5 | + 协议:dolphin MySQL 协议 (3306) | ||
| 6 | + 目标:openGauss 7.0.0-RC3 + dolphin 5.2 | ||
| 7 | +======================================== | ||
| 8 | + | ||
| 9 | +==================== 1. 确保目标数据库存在 ==================== | ||
| 10 | +02:04:05.827 [main] DEBUG com.zaxxer.hikari.HikariConfig - Driver class com.mysql.cj.jdbc.Driver found in Thread context class loader jdk.internal.loader.ClassLoaders$AppClassLoader@6d06d69c | ||
| 11 | +02:04:05.836 [main] DEBUG com.zaxxer.hikari.HikariConfig - InitPool - configuration: | ||
| 12 | +02:04:05.843 [main] DEBUG com.zaxxer.hikari.HikariConfig - allowPoolSuspension.............false | ||
| 13 | +02:04:05.843 [main] DEBUG com.zaxxer.hikari.HikariConfig - autoCommit......................true | ||
| 14 | +02:04:05.844 [main] DEBUG com.zaxxer.hikari.HikariConfig - catalog.........................none | ||
| 15 | +02:04:05.844 [main] DEBUG com.zaxxer.hikari.HikariConfig - connectionInitSql...............none | ||
| 16 | +02:04:05.844 [main] DEBUG com.zaxxer.hikari.HikariConfig - connectionTestQuery.............none | ||
| 17 | +02:04:05.844 [main] DEBUG com.zaxxer.hikari.HikariConfig - connectionTimeout...............30000 | ||
| 18 | +02:04:05.844 [main] DEBUG com.zaxxer.hikari.HikariConfig - dataSource......................none | ||
| 19 | +02:04:05.844 [main] DEBUG com.zaxxer.hikari.HikariConfig - dataSourceClassName.............none | ||
| 20 | +02:04:05.844 [main] DEBUG com.zaxxer.hikari.HikariConfig - dataSourceJNDI..................none | ||
| 21 | +02:04:05.845 [main] DEBUG com.zaxxer.hikari.HikariConfig - dataSourceProperties............{password=<masked>} | ||
| 22 | +02:04:05.846 [main] DEBUG com.zaxxer.hikari.HikariConfig - driverClassName................."com.mysql.cj.jdbc.Driver" | ||
| 23 | +02:04:05.846 [main] DEBUG com.zaxxer.hikari.HikariConfig - exceptionOverrideClassName......none | ||
| 24 | +02:04:05.846 [main] DEBUG com.zaxxer.hikari.HikariConfig - healthCheckProperties...........{} | ||
| 25 | +02:04:05.846 [main] DEBUG com.zaxxer.hikari.HikariConfig - healthCheckRegistry.............none | ||
| 26 | +02:04:05.846 [main] DEBUG com.zaxxer.hikari.HikariConfig - idleTimeout.....................600000 | ||
| 27 | +02:04:05.847 [main] DEBUG com.zaxxer.hikari.HikariConfig - initializationFailTimeout.......1 | ||
| 28 | +02:04:05.847 [main] DEBUG com.zaxxer.hikari.HikariConfig - isolateInternalQueries..........false | ||
| 29 | +02:04:05.847 [main] DEBUG com.zaxxer.hikari.HikariConfig - jdbcUrl.........................jdbc:mysql://172.18.15.52:3306/postgres?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true | ||
| 30 | +02:04:05.847 [main] DEBUG com.zaxxer.hikari.HikariConfig - keepaliveTime...................0 | ||
| 31 | +02:04:05.847 [main] DEBUG com.zaxxer.hikari.HikariConfig - leakDetectionThreshold..........0 | ||
| 32 | +02:04:05.847 [main] DEBUG com.zaxxer.hikari.HikariConfig - maxLifetime.....................1800000 | ||
| 33 | +02:04:05.847 [main] DEBUG com.zaxxer.hikari.HikariConfig - maximumPoolSize.................10 | ||
| 34 | +02:04:05.847 [main] DEBUG com.zaxxer.hikari.HikariConfig - metricRegistry..................none | ||
| 35 | +02:04:05.848 [main] DEBUG com.zaxxer.hikari.HikariConfig - metricsTrackerFactory...........none | ||
| 36 | +02:04:05.848 [main] DEBUG com.zaxxer.hikari.HikariConfig - minimumIdle.....................10 | ||
| 37 | +02:04:05.848 [main] DEBUG com.zaxxer.hikari.HikariConfig - password........................<masked> | ||
| 38 | +02:04:05.848 [main] DEBUG com.zaxxer.hikari.HikariConfig - poolName........................"InitPool" | ||
| 39 | +02:04:05.848 [main] DEBUG com.zaxxer.hikari.HikariConfig - readOnly........................false | ||
| 40 | +02:04:05.848 [main] DEBUG com.zaxxer.hikari.HikariConfig - registerMbeans..................false | ||
| 41 | +02:04:05.848 [main] DEBUG com.zaxxer.hikari.HikariConfig - scheduledExecutor...............none | ||
| 42 | +02:04:05.848 [main] DEBUG com.zaxxer.hikari.HikariConfig - schema..........................none | ||
| 43 | +02:04:05.848 [main] DEBUG com.zaxxer.hikari.HikariConfig - threadFactory...................internal | ||
| 44 | +02:04:05.849 [main] DEBUG com.zaxxer.hikari.HikariConfig - transactionIsolation............default | ||
| 45 | +02:04:05.849 [main] DEBUG com.zaxxer.hikari.HikariConfig - username........................"mysqluser" | ||
| 46 | +02:04:05.849 [main] DEBUG com.zaxxer.hikari.HikariConfig - validationTimeout...............5000 | ||
| 47 | +02:04:05.850 [main] INFO com.zaxxer.hikari.HikariDataSource - InitPool - Starting... | ||
| 48 | +02:04:06.034 [main] INFO com.zaxxer.hikari.pool.HikariPool - InitPool - Added connection com.mysql.cj.jdbc.ConnectionImpl@7995092a | ||
| 49 | +02:04:06.036 [main] INFO com.zaxxer.hikari.HikariDataSource - InitPool - Start completed. | ||
| 50 | +数据库 mysql_db 已存在,跳过创建。 | ||
| 51 | +02:04:06.064 [main] INFO com.zaxxer.hikari.HikariDataSource - InitPool - Shutdown initiated... | ||
| 52 | +02:04:06.064 [main] DEBUG com.zaxxer.hikari.pool.HikariPool - InitPool - Before shutdown stats (total=1, active=0, idle=1, waiting=0) | ||
| 53 | +02:04:06.064 [InitPool connection closer] DEBUG com.zaxxer.hikari.pool.PoolBase - InitPool - Closing connection com.mysql.cj.jdbc.ConnectionImpl@7995092a: (connection evicted) | ||
| 54 | +02:04:06.070 [main] DEBUG com.zaxxer.hikari.pool.HikariPool - InitPool - After shutdown stats (total=0, active=0, idle=0, waiting=0) | ||
| 55 | +02:04:06.070 [main] INFO com.zaxxer.hikari.HikariDataSource - InitPool - Shutdown completed. | ||
| 56 | + | ||
| 57 | +==================== 2. 连接池初始化 ==================== | ||
| 58 | +02:04:06.070 [main] DEBUG com.zaxxer.hikari.HikariConfig - Driver class com.mysql.cj.jdbc.Driver found in Thread context class loader jdk.internal.loader.ClassLoaders$AppClassLoader@6d06d69c | ||
| 59 | +02:04:06.070 [main] DEBUG com.zaxxer.hikari.HikariConfig - SqlCategoryPool - configuration: | ||
| 60 | +02:04:06.071 [main] DEBUG com.zaxxer.hikari.HikariConfig - allowPoolSuspension.............false | ||
| 61 | +02:04:06.071 [main] DEBUG com.zaxxer.hikari.HikariConfig - autoCommit......................true | ||
| 62 | +02:04:06.071 [main] DEBUG com.zaxxer.hikari.HikariConfig - catalog.........................none | ||
| 63 | +02:04:06.071 [main] DEBUG com.zaxxer.hikari.HikariConfig - connectionInitSql...............none | ||
| 64 | +02:04:06.071 [main] DEBUG com.zaxxer.hikari.HikariConfig - connectionTestQuery.............none | ||
| 65 | +02:04:06.072 [main] DEBUG com.zaxxer.hikari.HikariConfig - connectionTimeout...............30000 | ||
| 66 | +02:04:06.072 [main] DEBUG com.zaxxer.hikari.HikariConfig - dataSource......................none | ||
| 67 | +02:04:06.072 [main] DEBUG com.zaxxer.hikari.HikariConfig - dataSourceClassName.............none | ||
| 68 | +02:04:06.072 [main] DEBUG com.zaxxer.hikari.HikariConfig - dataSourceJNDI..................none | ||
| 69 | +02:04:06.072 [main] DEBUG com.zaxxer.hikari.HikariConfig - dataSourceProperties............{password=<masked>} | ||
| 70 | +02:04:06.072 [main] DEBUG com.zaxxer.hikari.HikariConfig - driverClassName................."com.mysql.cj.jdbc.Driver" | ||
| 71 | +02:04:06.072 [main] DEBUG com.zaxxer.hikari.HikariConfig - exceptionOverrideClassName......none | ||
| 72 | +02:04:06.072 [main] DEBUG com.zaxxer.hikari.HikariConfig - healthCheckProperties...........{} | ||
| 73 | +02:04:06.072 [main] DEBUG com.zaxxer.hikari.HikariConfig - healthCheckRegistry.............none | ||
| 74 | +02:04:06.072 [main] DEBUG com.zaxxer.hikari.HikariConfig - idleTimeout.....................600000 | ||
| 75 | +02:04:06.072 [main] DEBUG com.zaxxer.hikari.HikariConfig - initializationFailTimeout.......1 | ||
| 76 | +02:04:06.072 [main] DEBUG com.zaxxer.hikari.HikariConfig - isolateInternalQueries..........false | ||
| 77 | +02:04:06.072 [main] DEBUG com.zaxxer.hikari.HikariConfig - jdbcUrl.........................jdbc:mysql://172.18.15.52:3306/mysql_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true | ||
| 78 | +02:04:06.072 [main] DEBUG com.zaxxer.hikari.HikariConfig - keepaliveTime...................0 | ||
| 79 | +02:04:06.072 [main] DEBUG com.zaxxer.hikari.HikariConfig - leakDetectionThreshold..........0 | ||
| 80 | +02:04:06.072 [main] DEBUG com.zaxxer.hikari.HikariConfig - maxLifetime.....................1800000 | ||
| 81 | +02:04:06.072 [main] DEBUG com.zaxxer.hikari.HikariConfig - maximumPoolSize.................10 | ||
| 82 | +02:04:06.074 [main] DEBUG com.zaxxer.hikari.HikariConfig - metricRegistry..................none | ||
| 83 | +02:04:06.074 [main] DEBUG com.zaxxer.hikari.HikariConfig - metricsTrackerFactory...........none | ||
| 84 | +02:04:06.074 [main] DEBUG com.zaxxer.hikari.HikariConfig - minimumIdle.....................2 | ||
| 85 | +02:04:06.074 [main] DEBUG com.zaxxer.hikari.HikariConfig - password........................<masked> | ||
| 86 | +02:04:06.074 [main] DEBUG com.zaxxer.hikari.HikariConfig - poolName........................"SqlCategoryPool" | ||
| 87 | +02:04:06.074 [main] DEBUG com.zaxxer.hikari.HikariConfig - readOnly........................false | ||
| 88 | +02:04:06.074 [main] DEBUG com.zaxxer.hikari.HikariConfig - registerMbeans..................false | ||
| 89 | +02:04:06.074 [main] DEBUG com.zaxxer.hikari.HikariConfig - scheduledExecutor...............none | ||
| 90 | +02:04:06.074 [main] DEBUG com.zaxxer.hikari.HikariConfig - schema..........................none | ||
| 91 | +02:04:06.074 [main] DEBUG com.zaxxer.hikari.HikariConfig - threadFactory...................internal | ||
| 92 | +02:04:06.074 [main] DEBUG com.zaxxer.hikari.HikariConfig - transactionIsolation............default | ||
| 93 | +02:04:06.074 [main] DEBUG com.zaxxer.hikari.HikariConfig - username........................"mysqluser" | ||
| 94 | +02:04:06.074 [main] DEBUG com.zaxxer.hikari.HikariConfig - validationTimeout...............5000 | ||
| 95 | +02:04:06.074 [main] INFO com.zaxxer.hikari.HikariDataSource - SqlCategoryPool - Starting... | ||
| 96 | +02:04:06.091 [main] INFO com.zaxxer.hikari.pool.HikariPool - SqlCategoryPool - Added connection com.mysql.cj.jdbc.ConnectionImpl@48ae9b55 | ||
| 97 | +02:04:06.091 [main] INFO com.zaxxer.hikari.HikariDataSource - SqlCategoryPool - Start completed. | ||
| 98 | +HikariCP 连接池初始化完成:poolName=SqlCategoryPool, maximumPoolSize=10 | ||
| 99 | + | ||
| 100 | +==================== 3. DDL(Data Definition Language)验证 ==================== | ||
| 101 | +测试目的:验证 CREATE / ALTER / DROP / TRUNCATE 四种 DDL 操作通过 dolphin MySQL 协议正常执行。 | ||
| 102 | +[DDL-01] CREATE TABLE - 创建主测试表 | ||
| 103 | + PASS | CREATE TABLE sql_category_test 执行成功(含 SERIAL 主键、VARCHAR、INT、DECIMAL、TIMESTAMP 字段) | ||
| 104 | +[DDL-02] CREATE INDEX - 创建索引 | ||
| 105 | + PASS | CREATE INDEX idx_sql_category_test_name 执行成功 | ||
| 106 | +[DDL-03] ALTER TABLE ADD COLUMN - 增加列 | ||
| 107 | + PASS | ALTER TABLE ... ADD COLUMN remark TEXT 执行成功 | ||
| 108 | +[DDL-04] ALTER TABLE RENAME COLUMN - 重命名列 | ||
| 109 | + PASS | ALTER TABLE ... RENAME COLUMN remark TO description 执行成功 | ||
| 110 | +[DDL-05] TRUNCATE TABLE - 清空表数据 | ||
| 111 | + PASS | TRUNCATE TABLE sql_category_test 执行成功,当前行数=0 | ||
| 112 | +[DDL-06] DROP TABLE - 删除第二张测试表(先建再删) | ||
| 113 | + PASS | DROP TABLE sql_category_test_2 执行成功 | ||
| 114 | +[DDL-07] CREATE TABLE AS SELECT - 从查询结果创建表 | ||
| 115 | + PASS | CREATE TABLE sql_category_test_2 AS SELECT 执行成功,目标表行数=1 | ||
| 116 | +=> DDL 全部 7 项操作验证通过(CREATE/INDEX/ALTER ADD/RENAME/TRUNCATE/DROP/CTAS)。 | ||
| 117 | + | ||
| 118 | +==================== 4. DML(Data Manipulation Language)验证 ==================== | ||
| 119 | +测试目的:验证 INSERT / UPDATE / DELETE 三种 DML 操作及批量写入通过 dolphin MySQL 协议正常执行。 | ||
| 120 | +[DML-01] INSERT - 单行插入 | ||
| 121 | + PASS | INSERT 单行影响行数=1 (name=Alice, age=28, score=95.50) | ||
| 122 | +[DML-02] INSERT - 多行插入 | ||
| 123 | + PASS | INSERT 批量 3 行影响总行数=3 | ||
| 124 | +[DML-03] UPDATE - 条件更新 | ||
| 125 | + PASS | UPDATE 影响行数=1 (Alice age:28->29, score:95.50->96.00) | ||
| 126 | + 确认 | 更新后 Alice age=29, score=96.0 | ||
| 127 | +[DML-04] DELETE - 条件删除 | ||
| 128 | + PASS | DELETE 影响行数=3 (删除所有 BatchUser*) | ||
| 129 | +[DML-05] JDBC getGeneratedKeys - 获取自增主键(dolphin 兼容) | ||
| 130 | + INFO | 驱动未返回 GENERATED_KEYS(不影响插入成功) | ||
| 131 | +=> DML 共 4 项操作验证通过(单行INSERT/批量INSERT/UPDATE/DELETE);DML-05 getGeneratedKeys 为 INFO,不计入 PASS。 | ||
| 132 | + | ||
| 133 | +==================== 5. DQL(Data Query Language)验证 ==================== | ||
| 134 | +测试目的:验证 SELECT 及其子句(WHERE / ORDER BY / GROUP BY / 聚合函数 / LIMIT / JOIN)通过 dolphin MySQL 协议正常执行。 | ||
| 135 | +[DQL-01] SELECT * - 全量查询 | ||
| 136 | + PASS | SELECT * 返回 6 行 | ||
| 137 | + 2|ctas_src|25|88.5 | ||
| 138 | + 3|Alice|29|96.0 | ||
| 139 | + 7|Bob|35|87.25 | ||
| 140 | + 8|Charlie|22|78.0 | ||
| 141 | + 9|Diana|31|92.5 | ||
| 142 | + 10|Eve|27|85.75 | ||
| 143 | +[DQL-02] SELECT ... WHERE - 条件过滤 | ||
| 144 | +02:04:06.195 [SqlCategoryPool housekeeper] DEBUG com.zaxxer.hikari.pool.HikariPool - SqlCategoryPool - Before cleanup stats (total=1, active=1, idle=0, waiting=0) | ||
| 145 | +02:04:06.195 [SqlCategoryPool housekeeper] DEBUG com.zaxxer.hikari.pool.HikariPool - SqlCategoryPool - After cleanup stats (total=1, active=1, idle=0, waiting=0) | ||
| 146 | + PASS | WHERE age>=28 返回 3 行:Alice(age=29), Diana(age=31), Bob(age=35) | ||
| 147 | +[DQL-03] SELECT ... ORDER BY + LIMIT - 排序与分页 | ||
| 148 | + PASS | TOP 3 by score DESC:Alice(96.0) > Diana(92.5) > ctas_src(88.5) | ||
| 149 | +[DQL-04] 聚合函数 - COUNT / SUM / AVG / MIN / MAX | ||
| 150 | + PASS | COUNT=6, SUM(age)=169, AVG(score)=88.0, MIN(age)=22, MAX(age)=35 | ||
| 151 | +[DQL-05] GROUP BY + HAVING - 分组聚合与过滤 | ||
| 152 | +02:04:06.212 [SqlCategoryPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - SqlCategoryPool - Added connection com.mysql.cj.jdbc.ConnectionImpl@24e73d07 | ||
| 153 | + PASS | GROUP BY 分组数=2:young: cnt=1, avg_score=78.0; senior: cnt=5, avg_score=90.0 | ||
| 154 | +[DQL-06] SELECT DISTINCT - 去重 | ||
| 155 | + PASS | DISTINCT name 返回 6 个唯一值:Alice, Bob, Charlie, ctas_src, Diana, Eve | ||
| 156 | +[DQL-07] 子查询 - SELECT 中嵌套子查询 | ||
| 157 | + PASS | 年龄高于平均值的记录:Alice(29), Diana(31), Bob(35) | ||
| 158 | +=> DQL 全部 7 项查询验证通过(基础SELECT/WHERE/ORDER+LIMIT/聚合/GROUP+HAVING/DISTINCT/子查询)。 | ||
| 159 | + | ||
| 160 | +==================== 6. DCL(Data Control Language)验证 ==================== | ||
| 161 | +测试目的:验证 GRANT / REVOKE 权限管理操作通过 dolphin MySQL 协议正常执行。 | ||
| 162 | + 注意:DCL 操作需要当前用户具备相应权限(如超级用户或对象属主)。 | ||
| 163 | +[DCL-01] GRANT - 授予表级 SELECT 权限 | ||
| 164 | + PASS | GRANT SELECT ON TABLE sql_category_test TO mysqluser 执行成功 | ||
| 165 | +[DCL-02] GRANT - 授予多权限 | ||
| 166 | + PASS | GRANT INSERT,UPDATE,DELETE ON TABLE sql_category_test TO mysqluser 执行成功 | ||
| 167 | +[DCL-03] REVOKE - 收回权限 | ||
| 168 | + PASS | REVOKE INSERT,UPDATE,DELETE ON TABLE sql_category_test FROM mysqluser 执行成功 | ||
| 169 | +[DCL-04] 查询当前用户权限信息 | ||
| 170 | + PASS | 当前用户权限:SELECT(可转授), TRUNCATE(可转授), REFERENCES(可转授), TRIGGER(可转授) | ||
| 171 | +=> DCL 权限管理操作验证完成(GRANT/REVOKE/权限查询):PASS 4 项。 | ||
| 172 | + | ||
| 173 | +==================== 7. TCL(Transaction Control Language)验证 ==================== | ||
| 174 | +测试目的:验证 COMMIT / ROLLBACK / SAVEPOINT 事务控制操作通过 dolphin MySQL 协议正常执行。 | ||
| 175 | +[TCL-01] COMMIT - 显式提交事务 | ||
| 176 | +02:04:06.243 [SqlCategoryPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - SqlCategoryPool - Added connection com.mysql.cj.jdbc.ConnectionImpl@65da373c | ||
| 177 | + PASS | COMMIT 后 tcl_commit_test 数据可见,行数=1 | ||
| 178 | +[TCL-02] ROLLBACK - 回滚事务 | ||
| 179 | + PASS | ROLLBACK 后 tcl_rollback_test 数据不存在,行数=0 | ||
| 180 | +[TCL-03] SAVEPOINT + ROLLBACK TO SAVEPOINT - 部分回滚 | ||
| 181 | +02:04:06.257 [SqlCategoryPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - SqlCategoryPool - After adding stats (total=3, active=1, idle=2, waiting=0) | ||
| 182 | + PASS | ROLLBACK TO SAVEPOINT 后:sp_keep 行数=1(保留),sp_discard 行数=0(回滚) | ||
| 183 | +[TCL-04] 事务隔离级别确认 | ||
| 184 | + INFO | 当前事务隔离级别:READ-COMMITTED | ||
| 185 | +=> TCL 共 3 项事务控制验证通过(COMMIT/ROLLBACK/SAVEPOINT+RB_TO_SP);隔离级别查询为 INFO(仅查询,不计入 PASS)。 | ||
| 186 | + | ||
| 187 | +==================== 8. 并发连接验证 ==================== | ||
| 188 | +测试目的:启动多个线程同时从 HikariCP 连接池获取连接并执行 SQL,验证并发场景下连接不泄漏、SQL 正确执行。 | ||
| 189 | +启动 10 个并发线程... | ||
| 190 | +02:04:06.277 [pool-1-thread-5] DEBUG com.zaxxer.hikari.pool.PoolBase - SqlCategoryPool - Reset (autoCommit) on connection com.mysql.cj.jdbc.ConnectionImpl@48ae9b55 | ||
| 191 | +02:04:06.278 [pool-1-thread-4] DEBUG com.zaxxer.hikari.pool.PoolBase - SqlCategoryPool - Reset (autoCommit) on connection com.mysql.cj.jdbc.ConnectionImpl@65da373c | ||
| 192 | +02:04:06.278 [pool-1-thread-10] DEBUG com.zaxxer.hikari.pool.PoolBase - SqlCategoryPool - Reset (autoCommit) on connection com.mysql.cj.jdbc.ConnectionImpl@24e73d07 | ||
| 193 | +02:04:06.284 [pool-1-thread-6] DEBUG com.zaxxer.hikari.pool.PoolBase - SqlCategoryPool - Reset (autoCommit) on connection com.mysql.cj.jdbc.ConnectionImpl@65da373c | ||
| 194 | +02:04:06.284 [pool-1-thread-3] DEBUG com.zaxxer.hikari.pool.PoolBase - SqlCategoryPool - Reset (autoCommit) on connection com.mysql.cj.jdbc.ConnectionImpl@48ae9b55 | ||
| 195 | +02:04:06.284 [pool-1-thread-9] DEBUG com.zaxxer.hikari.pool.PoolBase - SqlCategoryPool - Reset (autoCommit) on connection com.mysql.cj.jdbc.ConnectionImpl@24e73d07 | ||
| 196 | +02:04:06.287 [SqlCategoryPool connection adder] DEBUG com.zaxxer.hikari.pool.HikariPool - SqlCategoryPool - Added connection com.mysql.cj.jdbc.ConnectionImpl@54f5f7c4 | ||
| 197 | +02:04:06.290 [pool-1-thread-2] DEBUG com.zaxxer.hikari.pool.PoolBase - SqlCategoryPool - Reset (autoCommit) on connection com.mysql.cj.jdbc.ConnectionImpl@24e73d07 | ||
| 198 | +02:04:06.290 [pool-1-thread-1] DEBUG com.zaxxer.hikari.pool.PoolBase - SqlCategoryPool - Reset (autoCommit) on connection com.mysql.cj.jdbc.ConnectionImpl@65da373c | ||
| 199 | +02:04:06.290 [pool-1-thread-7] DEBUG com.zaxxer.hikari.pool.PoolBase - SqlCategoryPool - Reset (autoCommit) on connection com.mysql.cj.jdbc.ConnectionImpl@48ae9b55 | ||
| 200 | +02:04:06.294 [pool-1-thread-8] DEBUG com.zaxxer.hikari.pool.PoolBase - SqlCategoryPool - Reset (autoCommit) on connection com.mysql.cj.jdbc.ConnectionImpl@54f5f7c4 | ||
| 201 | + 并发线程总数:10 | ||
| 202 | + 通过:10 / 失败:0 | ||
| 203 | + 线程池终止:正常 | ||
| 204 | + PASS | 10/10 个并发 worker 均完成 insert/select/update/delete/commit。 | ||
| 205 | +=> 并发连接验证通过。 | ||
| 206 | + | ||
| 207 | +==================== 9. 清理 DDL 残留表 ==================== | ||
| 208 | + 已清理 sql_category_test | ||
| 209 | + 已清理 sql_category_test_2 | ||
| 210 | + | ||
| 211 | +==================== 10. 最终结论 ==================== | ||
| 212 | ++--------+-----------------------------+--------------------------+ | ||
| 213 | +| 分类 | 操作类型 | 验证结果 | | ||
| 214 | ++--------+-----------------------------+--------------------------+ | ||
| 215 | +| DDL | CREATE / ALTER / DROP | PASS (7项) | | ||
| 216 | +| | TRUNCATE / CTAS | | | ||
| 217 | ++--------+-----------------------------+--------------------------+ | ||
| 218 | +| DML | INSERT / UPDATE / DELETE | PASS (4项) | | ||
| 219 | +| | 批量写入 | | | ||
| 220 | +| | getGeneratedKeys | INFO (1项) | | ||
| 221 | ++--------+-----------------------------+--------------------------+ | ||
| 222 | +| DQL | SELECT / WHERE / ORDER BY | PASS (7项) | | ||
| 223 | +| | 聚合 / GROUP BY / 子查询 | | | ||
| 224 | ++--------+-----------------------------+--------------------------+ | ||
| 225 | +| DCL | GRANT / REVOKE / 权限查询 | PASS (4项) | | ||
| 226 | ++--------+-----------------------------+--------------------------+ | ||
| 227 | +| TCL | COMMIT / ROLLBACK | PASS (3项) | | ||
| 228 | +| | SAVEPOINT+RB_TO_SP | | | ||
| 229 | +| | 隔离级别查询 | INFO (1项) | | ||
| 230 | ++--------+-----------------------------+--------------------------+ | ||
| 231 | +| 并发 | 10 worker 同时持连接执行SQL | PASS | | ||
| 232 | ++--------+-----------------------------+--------------------------+ | ||
| 233 | + | ||
| 234 | +DDL/DML/DQL/DCL/TCL + 并发连接验证通过;DML-05 getGeneratedKeys 与 TCL-04 隔离级别查询为 INFO,不计入 PASS。 | ||
| 235 | +HikariCP + MySQL Connector/J 经 dolphin MySQL 协议访问 openGauss B 兼容库功能完整可用。 | ||
| 236 | +02:04:06.302 [main] INFO com.zaxxer.hikari.HikariDataSource - SqlCategoryPool - Shutdown initiated... | ||
| 237 | +02:04:06.302 [main] DEBUG com.zaxxer.hikari.pool.HikariPool - SqlCategoryPool - Before shutdown stats (total=4, active=0, idle=4, waiting=0) | ||
| 238 | +02:04:06.302 [SqlCategoryPool connection closer] DEBUG com.zaxxer.hikari.pool.PoolBase - SqlCategoryPool - Closing connection com.mysql.cj.jdbc.ConnectionImpl@48ae9b55: (connection evicted) | ||
| 239 | +02:04:06.303 [SqlCategoryPool connection closer] DEBUG com.zaxxer.hikari.pool.PoolBase - SqlCategoryPool - Closing connection com.mysql.cj.jdbc.ConnectionImpl@24e73d07: (connection evicted) | ||
| 240 | +02:04:06.303 [SqlCategoryPool connection closer] DEBUG com.zaxxer.hikari.pool.PoolBase - SqlCategoryPool - Closing connection com.mysql.cj.jdbc.ConnectionImpl@65da373c: (connection evicted) | ||
| 241 | +02:04:06.303 [SqlCategoryPool connection closer] DEBUG com.zaxxer.hikari.pool.PoolBase - SqlCategoryPool - Closing connection com.mysql.cj.jdbc.ConnectionImpl@54f5f7c4: (connection evicted) | ||
| 242 | +02:04:06.304 [main] DEBUG com.zaxxer.hikari.pool.HikariPool - SqlCategoryPool - After shutdown stats (total=0, active=0, idle=0, waiting=0) | ||
| 243 | +02:04:06.304 [main] INFO com.zaxxer.hikari.HikariDataSource - SqlCategoryPool - Shutdown completed. | ||
| 244 | + | ||
| 245 | +连接池已关闭。全部验证结束。 | ||
| @@ -0,0 +1,237 @@ | |||
| 1 | +package com.linyu.ospp; | ||
| 2 | + | ||
| 3 | +import com.zaxxer.hikari.HikariConfig; | ||
| 4 | +import com.zaxxer.hikari.HikariDataSource; | ||
| 5 | +import com.zaxxer.hikari.HikariPoolMXBean; | ||
| 6 | + | ||
| 7 | +import javax.sql.DataSource; | ||
| 8 | +import java.sql.Connection; | ||
| 9 | +import java.sql.PreparedStatement; | ||
| 10 | +import java.sql.ResultSet; | ||
| 11 | +import java.util.concurrent.CountDownLatch; | ||
| 12 | +import java.util.concurrent.ExecutorService; | ||
| 13 | +import java.util.concurrent.Executors; | ||
| 14 | +import java.util.concurrent.TimeUnit; | ||
| 15 | +import java.util.concurrent.atomic.AtomicInteger; | ||
| 16 | +import java.util.concurrent.atomic.AtomicLong; | ||
| 17 | + | ||
| 18 | +/** | ||
| 19 | + * 大并发业务场景验证:模拟多用户并发下单(账户扣减 + 订单写入,事务化)。 | ||
| 20 | + * | ||
| 21 | + * 设计要点(回应"用大并发测一下、构建业务场景"): | ||
| 22 | + * 1. 业务场景:每个 worker 代表一个用户,在单个事务内完成"扣减自己账户余额 + 写入订单", | ||
| 23 | + * 比单纯建连/单条 CRUD 更贴近真实业务。 | ||
| 24 | + * 2. 大并发:worker 数远大于连接池 maximumPoolSize,制造连接池竞争,验证 | ||
| 25 | + * (a) 连接池上限不被突破(峰值活跃连接 <= maximumPoolSize) | ||
| 26 | + * (b) 高并发下事务全部正确提交、数据一致 | ||
| 27 | + * (c) 吞吐(QPS)与总耗时 | ||
| 28 | + * | ||
| 29 | + * 用法: java ...HighConcurrencyBusinessDemo [host] [workers] [db] [user] | ||
| 30 | + * 说明: 密码必须通过环境变量 DB_PASSWORD 设置 | ||
| 31 | + * 默认: host=127.0.0.1 workers=50 db=mysql_db user=mysqluser | ||
| 32 | + */ | ||
| 33 | +public class HighConcurrencyBusinessDemo { | ||
| 34 | + | ||
| 35 | + private static final int MAX_POOL = 10; // 故意小于并发数,制造连接池竞争 | ||
| 36 | + private static final int AMOUNT = 10; // 每笔订单金额 | ||
| 37 | + private static final long BIZ_SLEEP_MS = 20; // 模拟业务处理耗时(持连接期间),制造连接池排队 | ||
| 38 | + | ||
| 39 | + public static void main(String[] args) throws Exception { | ||
| 40 | + String host = args.length > 0 ? args[0] : "127.0.0.1"; | ||
| 41 | + int workers = args.length > 1 ? Integer.parseInt(args[1]) : 50; | ||
| 42 | + String db = args.length > 2 ? args[2] : "mysql_db"; | ||
| 43 | + String user = args.length > 3 ? args[3] : "mysqluser"; | ||
| 44 | + String pwd = System.getenv("DB_PASSWORD"); | ||
| 45 | + if (pwd == null || pwd.isEmpty()) { | ||
| 46 | + throw new IllegalStateException("请设置环境变量 DB_PASSWORD"); | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + HikariConfig cfg = new HikariConfig(); | ||
| 50 | + cfg.setJdbcUrl("jdbc:mysql://" + host + ":3306/" + db | ||
| 51 | + + "?allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true"); | ||
| 52 | + cfg.setUsername(user); | ||
| 53 | + cfg.setPassword(pwd); | ||
| 54 | + cfg.setMaximumPoolSize(MAX_POOL); | ||
| 55 | + cfg.setMinimumIdle(2); | ||
| 56 | + cfg.setConnectionTimeout(30000); | ||
| 57 | + cfg.setMaxLifetime(540000); | ||
| 58 | + cfg.setPoolName("HikariCP-HC-Stress"); | ||
| 59 | + final HikariDataSource ds; | ||
| 60 | + try { | ||
| 61 | + ds = new HikariDataSource(cfg); | ||
| 62 | + } catch (RuntimeException e) { | ||
| 63 | + System.err.println("连接池初始化失败,请检查配置文件: " + e.getMessage()); | ||
| 64 | + throw new IllegalStateException("连接池初始化失败", e); | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + if (workers <= 0) { | ||
| 68 | + throw new IllegalArgumentException("并发线程数必须大于 0,当前值:" + workers); | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + setup(ds, workers); | ||
| 72 | + | ||
| 73 | + AtomicInteger success = new AtomicInteger(); | ||
| 74 | + AtomicInteger fail = new AtomicInteger(); | ||
| 75 | + AtomicInteger peakActive = new AtomicInteger(); | ||
| 76 | + AtomicLong totalBalance = new AtomicLong(); | ||
| 77 | + | ||
| 78 | + CountDownLatch startGate = new CountDownLatch(1); | ||
| 79 | + CountDownLatch doneGate = new CountDownLatch(workers); | ||
| 80 | + | ||
| 81 | + ExecutorService es = Executors.newFixedThreadPool(workers); | ||
| 82 | + | ||
| 83 | + long t0 = System.currentTimeMillis(); | ||
| 84 | + for (int i = 0; i < workers; i++) { | ||
| 85 | + final int wid = i + 1; | ||
| 86 | + es.submit(() -> { | ||
| 87 | + try { | ||
| 88 | + startGate.await(); | ||
| 89 | + } catch (InterruptedException ignored) { | ||
| 90 | + } | ||
| 91 | + try (Connection c = ds.getConnection()) { | ||
| 92 | + HikariPoolMXBean mx = ds.getHikariPoolMXBean(); | ||
| 93 | + if (mx != null) { | ||
| 94 | + int cur = mx.getActiveConnections(); | ||
| 95 | + int prev; | ||
| 96 | + do { | ||
| 97 | + prev = peakActive.get(); | ||
| 98 | + } while (cur > prev && !peakActive.compareAndSet(prev, cur)); | ||
| 99 | + } | ||
| 100 | + businessTx(c, wid); | ||
| 101 | + success.incrementAndGet(); | ||
| 102 | + } catch (Exception e) { | ||
| 103 | + fail.incrementAndGet(); | ||
| 104 | + System.err.println("worker " + wid + " FAILED: " + e.getMessage()); | ||
| 105 | + } finally { | ||
| 106 | + doneGate.countDown(); | ||
| 107 | + } | ||
| 108 | + }); | ||
| 109 | + } | ||
| 110 | + startGate.countDown(); | ||
| 111 | + boolean finished = doneGate.await(180, TimeUnit.SECONDS); | ||
| 112 | + long t1 = System.currentTimeMillis(); | ||
| 113 | + | ||
| 114 | + boolean verified = false; | ||
| 115 | + if (finished) { | ||
| 116 | + // 正常完成:优雅关闭线程池(此时所有任务已结束),再做一致性校验 | ||
| 117 | + es.shutdown(); | ||
| 118 | + verified = verify(ds, workers, success.get(), totalBalance); | ||
| 119 | + } else { | ||
| 120 | + // 超时:180s 内未全部完成,先打印中间结果,再强制中断仍在运行的 worker, | ||
| 121 | + // 并等待其释放连接,避免粗暴 close 数据源导致线程被突然打断而产生难以排查的异常。 | ||
| 122 | + System.err.println("超时:180s 内仍有 worker 未完成,强制中断剩余任务。"); | ||
| 123 | + es.shutdownNow(); | ||
| 124 | + try { | ||
| 125 | + es.awaitTermination(10, TimeUnit.SECONDS); | ||
| 126 | + } catch (InterruptedException ignored) { | ||
| 127 | + Thread.currentThread().interrupt(); | ||
| 128 | + } | ||
| 129 | + } | ||
| 130 | + | ||
| 131 | + System.out.println(); | ||
| 132 | + System.out.println("==================== 大并发业务场景 验证结果 ===================="); | ||
| 133 | + System.out.println("目标实例 : " + host + ":3306/" + db); | ||
| 134 | + System.out.println("并发 worker : " + workers); | ||
| 135 | + System.out.println("连接池上限 : " + MAX_POOL + " (maximumPoolSize)"); | ||
| 136 | + System.out.println("完成状态 : " + (finished ? "全部在 180s 内完成" : "超时未完成!")); | ||
| 137 | + System.out.println("成功/失败 : " + success.get() + " / " + fail.get()); | ||
| 138 | + System.out.println("峰值活跃连接 : " + peakActive.get() + " (必须 <= 连接池上限 " + MAX_POOL + ")"); | ||
| 139 | + System.out.println("总耗时 : " + (t1 - t0) + " ms"); | ||
| 140 | + System.out.println("吞吐 QPS : " + String.format("%.1f", workers * 1000.0 / (t1 - t0))); | ||
| 141 | + if (finished) { | ||
| 142 | + System.out.println("账户总余额 : " + totalBalance.get() + " (期望=" + (workers * 1000L - (long) success.get() * AMOUNT) + ")"); | ||
| 143 | + } else { | ||
| 144 | + System.out.println("账户总余额 : 超时未完成,余额校验已跳过"); | ||
| 145 | + } | ||
| 146 | + System.out.println("==============================================================="); | ||
| 147 | + | ||
| 148 | + try { | ||
| 149 | + ds.close(); | ||
| 150 | + } catch (Exception ignored) { | ||
| 151 | + } | ||
| 152 | + | ||
| 153 | + // 退出码综合判定:任一条件不满足即视为验证失败。 | ||
| 154 | + // - finished=false:超时未完成(连接池竞争下出现永久阻塞或任务卡死) | ||
| 155 | + // - fail>0:存在失败的事务 | ||
| 156 | + // - peakActive>MAX_POOL:峰值活跃连接突破连接池上限 | ||
| 157 | + // - verified=false:一致性校验未通过(订单数/账户余额与预期不符) | ||
| 158 | + boolean ok = finished && fail.get() == 0 && peakActive.get() <= MAX_POOL && verified; | ||
| 159 | + if (!ok) { | ||
| 160 | + System.err.println("== 大并发业务场景验证 FAILED =="); | ||
| 161 | + System.exit(1); | ||
| 162 | + } | ||
| 163 | + System.out.println("== 大并发业务场景验证 PASSED =="); | ||
| 164 | + System.exit(0); | ||
| 165 | + } | ||
| 166 | + | ||
| 167 | + /** 建表 + 初始化账户(每个 worker 一个账户,余额 1000)。 */ | ||
| 168 | + private static void setup(DataSource ds, int workers) throws Exception { | ||
| 169 | + try (Connection c = ds.getConnection(); java.sql.Statement st = c.createStatement()) { | ||
| 170 | + st.execute("CREATE TABLE IF NOT EXISTS hc_accounts (id INT PRIMARY KEY, balance INT)"); | ||
| 171 | + st.execute("CREATE TABLE IF NOT EXISTS hc_orders (id INT PRIMARY KEY, user_id INT, amount INT)"); | ||
| 172 | + st.execute("TRUNCATE TABLE hc_orders"); | ||
| 173 | + st.execute("DELETE FROM hc_accounts"); | ||
| 174 | + try (PreparedStatement ps = c.prepareStatement("INSERT INTO hc_accounts(id,balance) VALUES(?,?)")) { | ||
| 175 | + for (int i = 1; i <= workers; i++) { | ||
| 176 | + ps.setInt(1, i); | ||
| 177 | + ps.setInt(2, 1000); | ||
| 178 | + ps.addBatch(); | ||
| 179 | + } | ||
| 180 | + ps.executeBatch(); | ||
| 181 | + } | ||
| 182 | + } | ||
| 183 | + } | ||
| 184 | + | ||
| 185 | + /** 单笔业务事务:扣减自己账户 + 写入订单,提交。 */ | ||
| 186 | + private static void businessTx(Connection c, int wid) throws Exception { | ||
| 187 | + c.setAutoCommit(false); | ||
| 188 | + try { | ||
| 189 | + try (PreparedStatement u = c.prepareStatement("UPDATE hc_accounts SET balance=balance-? WHERE id=?")) { | ||
| 190 | + u.setInt(1, AMOUNT); | ||
| 191 | + u.setInt(2, wid); | ||
| 192 | + if (u.executeUpdate() != 1) { | ||
| 193 | + throw new RuntimeException("账户更新失败 wid=" + wid); | ||
| 194 | + } | ||
| 195 | + } | ||
| 196 | + try (PreparedStatement io = c.prepareStatement("INSERT INTO hc_orders(id,user_id,amount) VALUES(?,?,?)")) { | ||
| 197 | + io.setInt(1, wid); | ||
| 198 | + io.setInt(2, wid); | ||
| 199 | + io.setInt(3, AMOUNT); | ||
| 200 | + io.executeUpdate(); | ||
| 201 | + } | ||
| 202 | + Thread.sleep(BIZ_SLEEP_MS); // 模拟业务处理(持连接期间),拉长占用以制造连接池排队 | ||
| 203 | + c.commit(); | ||
| 204 | + } catch (Exception e) { | ||
| 205 | + c.rollback(); | ||
| 206 | + throw e; | ||
| 207 | + } finally { | ||
| 208 | + c.setAutoCommit(true); | ||
| 209 | + } | ||
| 210 | + } | ||
| 211 | + | ||
| 212 | + /** 一致性校验:订单数=worker 数;账户总余额=初始-扣减。返回 true 表示两项校验均通过。 */ | ||
| 213 | + private static boolean verify(DataSource ds, int workers, int success, AtomicLong outBalance) throws Exception { | ||
| 214 | + boolean allPass = true; | ||
| 215 | + try (Connection c = ds.getConnection(); java.sql.Statement st = c.createStatement()) { | ||
| 216 | + try (ResultSet r1 = st.executeQuery("SELECT COUNT(*) FROM hc_orders")) { | ||
| 217 | + r1.next(); | ||
| 218 | + long orders = r1.getLong(1); | ||
| 219 | + boolean pass = orders == success; | ||
| 220 | + allPass &= pass; | ||
| 221 | + System.out.println("[校验] 订单数=" + orders + " (期望=" + success + ") -> " | ||
| 222 | + + (pass ? "PASS" : "FAIL")); | ||
| 223 | + } | ||
| 224 | + try (ResultSet r2 = st.executeQuery("SELECT SUM(balance) FROM hc_accounts")) { | ||
| 225 | + r2.next(); | ||
| 226 | + long sum = r2.getLong(1); | ||
| 227 | + outBalance.set(sum); | ||
| 228 | + long expect = workers * 1000L - (long) success * AMOUNT; | ||
| 229 | + boolean pass = sum == expect; | ||
| 230 | + allPass &= pass; | ||
| 231 | + System.out.println("[校验] 账户总余额=" + sum + " (期望=" + expect + ") -> " | ||
| 232 | + + (pass ? "PASS" : "FAIL")); | ||
| 233 | + } | ||
| 234 | + } | ||
| 235 | + return allPass; | ||
| 236 | + } | ||
| 237 | +} | ||
| @@ -0,0 +1,646 @@ | |||
| 1 | +package com.linyu.ospp; | ||
| 2 | + | ||
| 3 | +import com.zaxxer.hikari.HikariDataSource; | ||
| 4 | +import com.zaxxer.hikari.HikariPoolMXBean; | ||
| 5 | +import org.springframework.beans.factory.annotation.Value; | ||
| 6 | +import org.springframework.boot.CommandLineRunner; | ||
| 7 | +import org.springframework.jdbc.core.JdbcTemplate; | ||
| 8 | +import org.springframework.stereotype.Component; | ||
| 9 | +import org.springframework.transaction.support.TransactionTemplate; | ||
| 10 | + | ||
| 11 | +import javax.sql.DataSource; | ||
| 12 | +import java.sql.Connection; | ||
| 13 | +import java.sql.PreparedStatement; | ||
| 14 | +import java.sql.ResultSet; | ||
| 15 | +import java.sql.SQLTransientConnectionException; | ||
| 16 | +import java.util.ArrayList; | ||
| 17 | +import java.util.Arrays; | ||
| 18 | +import java.util.HashSet; | ||
| 19 | +import java.util.List; | ||
| 20 | +import java.util.Set; | ||
| 21 | +import java.util.concurrent.CountDownLatch; | ||
| 22 | +import java.util.concurrent.ExecutionException; | ||
| 23 | +import java.util.concurrent.ExecutorService; | ||
| 24 | +import java.util.concurrent.Executors; | ||
| 25 | +import java.util.concurrent.Future; | ||
| 26 | +import java.util.concurrent.TimeUnit; | ||
| 27 | +import java.util.concurrent.TimeoutException; | ||
| 28 | +import java.util.concurrent.atomic.AtomicInteger; | ||
| 29 | + | ||
| 30 | + | ||
| 31 | +public class HikariVerificationRunner implements CommandLineRunner { | ||
| 32 | + // user 是 MySQL 关键字,按文档使用反引号保持与 openGauss B 兼容语法一致。 | ||
| 33 | + private static final String TABLE_NAME = "`user`"; | ||
| 34 | + private static final List<String> REQUIRED_INITIAL_USERS = Arrays.asList("张三", "李四", "王五"); | ||
| 35 | + private static final String REQUIRED_HIKARI_VERSION_PREFIX = "5."; | ||
| 36 | + private int sectionNo = 1; | ||
| 37 | + | ||
| 38 | + private final boolean enabled; | ||
| 39 | + private final DataSource dataSource; | ||
| 40 | + private final JdbcTemplate jdbcTemplate; | ||
| 41 | + private final TransactionTemplate transactionTemplate; | ||
| 42 | + | ||
| 43 | + public HikariVerificationRunner( boolean enabled, | ||
| 44 | + DataSource dataSource, | ||
| 45 | + JdbcTemplate jdbcTemplate, | ||
| 46 | + TransactionTemplate transactionTemplate) { | ||
| 47 | + this.enabled = enabled; | ||
| 48 | + this.dataSource = dataSource; | ||
| 49 | + this.jdbcTemplate = jdbcTemplate; | ||
| 50 | + this.transactionTemplate = transactionTemplate; | ||
| 51 | + } | ||
| 52 | + | ||
| 53 | + | ||
| 54 | + public void run(String... args) throws Exception { | ||
| 55 | + // 测试类中会关闭该开关,避免普通 mvn test 必须依赖本机 openGauss 实例。 | ||
| 56 | + // 直接运行 main 时 application.yml 默认开启,方便一键验证文档步骤。 | ||
| 57 | + if (!enabled) { | ||
| 58 | + System.out.println("OSPP HikariCP 验证已关闭;如需运行,请设置 OSPP_VERIFY_ENABLED=true。"); | ||
| 59 | + return; | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | + printSection("验证 HikariCP 数据源自动装配"); | ||
| 63 | + printPurpose("确认 Spring Boot 2.5.6 通过 spring-boot-starter-jdbc 自动创建 HikariDataSource,并读取到预期连接池参数。"); | ||
| 64 | + HikariDataSource hikariDataSource = verifyDataSourceInfo(); | ||
| 65 | + | ||
| 66 | + printSection("验证数据库连接与基础 SQL"); | ||
| 67 | + printPurpose("确认 MySQL Connector/J 可以通过 dolphin MySQL 协议端口连接 openGauss,并执行基础查询。"); | ||
| 68 | + verifyConnection(); | ||
| 69 | + | ||
| 70 | + printSection("验证 dolphin 插件与 MySQL 协议配置"); | ||
| 71 | + printPurpose("直接查询 openGauss 系统表,确认 dolphin 插件已经安装,并确认 dolphin MySQL 协议开关和监听端口配置存在。"); | ||
| 72 | + verifyDolphinPlugin(); | ||
| 73 | + | ||
| 74 | + printSection("验证 dolphin 客户端库名与 schema 映射"); | ||
| 75 | + printPurpose("确认 JDBC URL 中的 mysql_test_db 能映射到 openGauss B 兼容库中的 mysql_test_db schema,避免文档只写库名但实际落错 schema。"); | ||
| 76 | + verifySchemaMapping(); | ||
| 77 | + | ||
| 78 | + printSection("清理历史验证数据"); | ||
| 79 | + printPurpose("删除上一次验证遗留的临时数据,保证本次 CRUD、事务、并发验证结果可重复。"); | ||
| 80 | + cleanupPreviousRunRows(); | ||
| 81 | + | ||
| 82 | + printSection("验证初始业务表查询"); | ||
| 83 | + printPurpose("确认文档准备步骤中的 user 表可以通过 MySQL 协议查询,并且至少包含张三、李四、王五三条基础数据。"); | ||
| 84 | + verifyInitialUsers(); | ||
| 85 | + | ||
| 86 | + printSection("验证 INSERT 插入能力"); | ||
| 87 | + printPurpose("向 user 表插入一条临时数据,确认 MySQL 协议下的写入链路可用。"); | ||
| 88 | + insertUser("zhaoliu", 18); | ||
| 89 | + selectUsers("插入后"); | ||
| 90 | + | ||
| 91 | + printSection("验证 UPDATE 更新能力"); | ||
| 92 | + printPurpose("按 name 找到刚插入的数据并更新,确认 MySQL 协议下的更新链路可用。"); | ||
| 93 | + Integer id = getUserIdByName("zhaoliu"); | ||
| 94 | + if (id == null) { | ||
| 95 | + throw new IllegalStateException("插入后的 zhaoliu 数据没有查到,INSERT 验证结果不可信。"); | ||
| 96 | + } | ||
| 97 | + | ||
| 98 | + updateUser(id, "zhaoliuliuliu", 28); | ||
| 99 | + assertUserExists("zhaoliuliuliu", 28); | ||
| 100 | + selectUsers("更新后"); | ||
| 101 | + | ||
| 102 | + printSection("验证 Spring 事务提交"); | ||
| 103 | + printPurpose("通过 TransactionTemplate 插入并提交一条数据,确认 Spring 事务管理器和 Hikari 连接协同正常。"); | ||
| 104 | + verifyTransaction(); | ||
| 105 | + | ||
| 106 | + printSection("验证连接数超出 Hikari 配置后的等待行为"); | ||
| 107 | + printPurpose("先借满 maximumPoolSize 条连接,再申请第 maximumPoolSize + 1 条连接,确认 HikariCP 不会突破最大连接数,而是等待空闲连接。"); | ||
| 108 | + verifyPoolLimit(hikariDataSource); | ||
| 109 | + | ||
| 110 | + printSection("验证并发真实数据库操作"); | ||
| 111 | + printPurpose("启动 10 个 worker,每个 worker 持有自己的连接,并完成 insert/select/update/select/delete/commit,证明不是只拿到连接就算并发通过。"); | ||
| 112 | + verifyConcurrentOperations(); | ||
| 113 | + | ||
| 114 | + printSection("验证 DELETE 删除能力与最终清理"); | ||
| 115 | + printPurpose("删除本次单线程 CRUD 临时数据,并清理事务、并发、连接数验证产生的临时数据。"); | ||
| 116 | + deleteUser(id); | ||
| 117 | + cleanupPreviousRunRows(); | ||
| 118 | + selectUsers("最终清理后"); | ||
| 119 | + | ||
| 120 | + printSection("验证结论"); | ||
| 121 | + System.out.println("验证通过:Spring Boot 2.5.6 + HikariCP + MySQL Connector/J 可以通过 dolphin MySQL 协议访问 openGauss,并完成查询、增删改、事务、连接池上限和真实并发数据库操作验证。"); | ||
| 122 | + } | ||
| 123 | + | ||
| 124 | + private void printSection(String title) { | ||
| 125 | + System.out.println(); | ||
| 126 | + System.out.println("==================== " + sectionNo++ + ". " + title + " ===================="); | ||
| 127 | + } | ||
| 128 | + | ||
| 129 | + private void printPurpose(String purpose) { | ||
| 130 | + System.out.println("测试目的:" + purpose); | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + private HikariDataSource verifyDataSourceInfo() { | ||
| 134 | + // Spring Boot 2.x 在引入 spring-boot-starter-jdbc 后默认选择 HikariCP。 | ||
| 135 | + // 这里显式断言数据源类型,防止项目后续依赖变化导致连接池被替换。 | ||
| 136 | + if (!(dataSource instanceof HikariDataSource)) { | ||
| 137 | + throw new IllegalStateException("数据源类型错误:预期是 HikariDataSource,实际是 " + dataSource.getClass().getName()); | ||
| 138 | + } | ||
| 139 | + | ||
| 140 | + HikariDataSource hikari = (HikariDataSource) dataSource; | ||
| 141 | + System.out.println("数据源实现类:" + hikari.getClass().getName()); | ||
| 142 | + String hikariVersion = resolveHikariVersion(); | ||
| 143 | + System.out.println("HikariCP 运行版本:" + hikariVersion); | ||
| 144 | + System.out.println("HikariCP 加载位置:" + resolveHikariLocation()); | ||
| 145 | + System.out.println("连接池名称:" + hikari.getPoolName()); | ||
| 146 | + System.out.println("最大连接数 maximumPoolSize:" + hikari.getMaximumPoolSize()); | ||
| 147 | + System.out.println("最小空闲连接数 minimumIdle:" + hikari.getMinimumIdle()); | ||
| 148 | + System.out.println("获取连接超时时间 connectionTimeout(ms):" + hikari.getConnectionTimeout()); | ||
| 149 | + System.out.println("空闲连接保留时间 idleTimeout(ms):" + hikari.getIdleTimeout()); | ||
| 150 | + System.out.println("连接最大生命周期 maxLifetime(ms):" + hikari.getMaxLifetime()); | ||
| 151 | + System.out.println("连接保活时间 keepaliveTime(ms):" + hikari.getKeepaliveTime()); | ||
| 152 | + | ||
| 153 | + if (hikari.getMaximumPoolSize() <= 0) { | ||
| 154 | + throw new IllegalStateException("HikariCP 最大连接数配置无效:" + hikari.getMaximumPoolSize()); | ||
| 155 | + } | ||
| 156 | + if (hikariVersion == null || !hikariVersion.startsWith(REQUIRED_HIKARI_VERSION_PREFIX)) { | ||
| 157 | + throw new IllegalStateException("HikariCP 版本校验失败:预期运行 5.x 版本,实际为 " + hikariVersion); | ||
| 158 | + } | ||
| 159 | + if (hikari.getMaxLifetime() <= hikari.getKeepaliveTime()) { | ||
| 160 | + throw new IllegalStateException("HikariCP maxLifetime 应大于 keepaliveTime,否则连接保活配置没有意义。"); | ||
| 161 | + } | ||
| 162 | + System.out.println("数据源装配校验通过:当前使用 HikariCP,连接池参数已生效。"); | ||
| 163 | + return hikari; | ||
| 164 | + } | ||
| 165 | + | ||
| 166 | + private String resolveHikariVersion() { | ||
| 167 | + String implementationVersion = HikariDataSource.class.getPackage().getImplementationVersion(); | ||
| 168 | + if (implementationVersion != null) { | ||
| 169 | + return implementationVersion; | ||
| 170 | + } | ||
| 171 | + | ||
| 172 | + // mvn spring-boot:run 使用展开 classpath 时,Manifest 版本可能读不到; | ||
| 173 | + // 此时从 HikariCP jar 文件名中解析版本,仍然能证明运行时加载的是哪个版本。 | ||
| 174 | + String location = resolveHikariLocation(); | ||
| 175 | + String marker = "HikariCP-"; | ||
| 176 | + int versionStart = location.indexOf(marker); | ||
| 177 | + int jarSuffix = location.indexOf(".jar", versionStart); | ||
| 178 | + if (versionStart >= 0 && jarSuffix > versionStart) { | ||
| 179 | + return location.substring(versionStart + marker.length(), jarSuffix); | ||
| 180 | + } | ||
| 181 | + return null; | ||
| 182 | + } | ||
| 183 | + | ||
| 184 | + private String resolveHikariLocation() { | ||
| 185 | + try { | ||
| 186 | + return HikariDataSource.class.getProtectionDomain().getCodeSource().getLocation().toString(); | ||
| 187 | + } catch (Exception e) { | ||
| 188 | + return "无法读取:" + e.getMessage(); | ||
| 189 | + } | ||
| 190 | + } | ||
| 191 | + | ||
| 192 | + private void verifyConnection() throws Exception { | ||
| 193 | + // 同时验证 JDBC4 isValid、简单 SELECT、version(),覆盖连接池到数据库的最短链路。 | ||
| 194 | + try (Connection connection = dataSource.getConnection()) { | ||
| 195 | + boolean valid = connection.isValid(5); | ||
| 196 | + System.out.println("JDBC 连接有效性校验结果:" + valid); | ||
| 197 | + if (!valid) { | ||
| 198 | + throw new IllegalStateException("JDBC 连接有效性校验失败。"); | ||
| 199 | + } | ||
| 200 | + } | ||
| 201 | + | ||
| 202 | + Integer one = jdbcTemplate.queryForObject("SELECT 1", Integer.class); | ||
| 203 | + System.out.println("基础 SQL 执行结果:SELECT 1 = " + one); | ||
| 204 | + if (one == null || one != 1) { | ||
| 205 | + throw new IllegalStateException("基础 SQL 校验失败:SELECT 1 返回值为 " + one); | ||
| 206 | + } | ||
| 207 | + | ||
| 208 | + String version = jdbcTemplate.queryForObject("SELECT version()", String.class); | ||
| 209 | + System.out.println("数据库版本信息:" + version); | ||
| 210 | + if (version == null || !version.toLowerCase().contains("opengauss")) { | ||
| 211 | + throw new IllegalStateException("数据库版本信息中没有识别到 openGauss,实际返回:" + version); | ||
| 212 | + } | ||
| 213 | + System.out.println("数据库连接与基础 SQL 校验通过。"); | ||
| 214 | + } | ||
| 215 | + | ||
| 216 | + private void verifyDolphinPlugin() { | ||
| 217 | + // pg_extension 能直接证明当前 openGauss 实例安装了 dolphin 插件; | ||
| 218 | + // pg_settings 能证明服务端已经配置 MySQL 协议开关和 dolphin 监听端口。 | ||
| 219 | + //noinspection SqlResolve | ||
| 220 | + Integer extensionCount = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM pg_extension WHERE extname = ?", | ||
| 221 | + Integer.class, | ||
| 222 | + "dolphin"); | ||
| 223 | + System.out.println("dolphin 插件安装数量:" + extensionCount); | ||
| 224 | + if (extensionCount == null || extensionCount < 1) { | ||
| 225 | + throw new IllegalStateException("dolphin 插件校验失败:pg_extension 中没有查到 dolphin。"); | ||
| 226 | + } | ||
| 227 | + | ||
| 228 | + String enableDolphinProto = queryPgSetting("enable_dolphin_proto"); | ||
| 229 | + String dolphinServerPort = queryPgSetting("dolphin_server_port"); | ||
| 230 | + System.out.println("enable_dolphin_proto 配置值:" + enableDolphinProto); | ||
| 231 | + System.out.println("dolphin_server_port 配置值:" + dolphinServerPort); | ||
| 232 | + if (!"on".equalsIgnoreCase(enableDolphinProto)) { | ||
| 233 | + throw new IllegalStateException("dolphin MySQL 协议开关校验失败:enable_dolphin_proto=" + enableDolphinProto); | ||
| 234 | + } | ||
| 235 | + if (!"3306".equals(dolphinServerPort)) { | ||
| 236 | + throw new IllegalStateException("dolphin MySQL 协议端口校验失败:dolphin_server_port=" + dolphinServerPort); | ||
| 237 | + } | ||
| 238 | + | ||
| 239 | + System.out.println("dolphin 插件与 MySQL 协议配置校验通过。"); | ||
| 240 | + } | ||
| 241 | + | ||
| 242 | + private String queryPgSetting(String name) { | ||
| 243 | + //noinspection SqlResolve | ||
| 244 | + List<String> values = jdbcTemplate.query("SELECT setting FROM pg_settings WHERE name = ?", | ||
| 245 | + (rs, rowNum) -> rs.getString("setting"), | ||
| 246 | + name); | ||
| 247 | + if (values.isEmpty()) { | ||
| 248 | + throw new IllegalStateException("配置项校验失败:pg_settings 中没有查到 " + name + "。"); | ||
| 249 | + } | ||
| 250 | + return values.get(0); | ||
| 251 | + } | ||
| 252 | + | ||
| 253 | + private void verifySchemaMapping() { | ||
| 254 | + // dolphin MySQL 协议下,客户端 URL 中的 database 名称会影响当前 schema。 | ||
| 255 | + // 这里把 DATABASE() 和 current_schema() 同时打印出来,避免文档把数据库和 schema 映射关系写模糊。 | ||
| 256 | + //noinspection SqlResolve | ||
| 257 | + jdbcTemplate.query("SELECT DATABASE() AS client_database, current_schema() AS current_schema_name", rs -> { | ||
| 258 | + String clientDatabase = rs.getString("client_database"); | ||
| 259 | + String currentSchema = rs.getString("current_schema_name"); | ||
| 260 | + System.out.println("JDBC URL 客户端库名 DATABASE():" + clientDatabase); | ||
| 261 | + System.out.println("openGauss 当前 schema current_schema():" + currentSchema); | ||
| 262 | + if (!"mysql_test_db".equals(clientDatabase)) { | ||
| 263 | + throw new IllegalStateException("客户端库名映射异常:预期 DATABASE() 为 mysql_test_db,实际为 " + clientDatabase); | ||
| 264 | + } | ||
| 265 | + if (!"mysql_test_db".equals(currentSchema)) { | ||
| 266 | + throw new IllegalStateException("当前 schema 映射异常:预期 current_schema() 为 mysql_test_db,实际为 " + currentSchema); | ||
| 267 | + } | ||
| 268 | + }); | ||
| 269 | + System.out.println("dolphin 库名与 schema 映射校验通过。"); | ||
| 270 | + } | ||
| 271 | + | ||
| 272 | + private void cleanupPreviousRunRows() { | ||
| 273 | + // 只删除本验证程序生成的临时数据,不影响文档准备阶段插入的 3 条基础数据。 | ||
| 274 | + int fixedRows = jdbcTemplate.update("DELETE FROM " + TABLE_NAME + " WHERE name IN (?, ?, ?, ?)", | ||
| 275 | + "zhaoliu", "zhaoliuliuliu", "spring-transaction", "pool-limit-user"); | ||
| 276 | + int concurrentRows = jdbcTemplate.update("DELETE FROM " + TABLE_NAME + " WHERE name LIKE ?", | ||
| 277 | + "concurrent-user-%"); | ||
| 278 | + System.out.println("历史临时数据清理结果:固定名称数据 " + fixedRows + " 行,并发名称数据 " + concurrentRows + " 行。"); | ||
| 279 | + } | ||
| 280 | + | ||
| 281 | + private void insertUser(String name, int age) { | ||
| 282 | + int rows = jdbcTemplate.update("INSERT INTO " + TABLE_NAME + " (name, age) VALUES (?, ?)", name, age); | ||
| 283 | + System.out.println("INSERT 执行影响行数:" + rows); | ||
| 284 | + if (rows != 1) { | ||
| 285 | + throw new IllegalStateException("INSERT 校验失败:预期影响 1 行,实际影响 " + rows + " 行。"); | ||
| 286 | + } | ||
| 287 | + } | ||
| 288 | + | ||
| 289 | + private void updateUser(int id, String name, int age) { | ||
| 290 | + int rows = jdbcTemplate.update("UPDATE " + TABLE_NAME + " SET name = ?, age = ? WHERE id = ?", name, age, id); | ||
| 291 | + System.out.println("UPDATE 执行影响行数:" + rows); | ||
| 292 | + if (rows != 1) { | ||
| 293 | + throw new IllegalStateException("UPDATE 校验失败:预期影响 1 行,实际影响 " + rows + " 行。"); | ||
| 294 | + } | ||
| 295 | + } | ||
| 296 | + | ||
| 297 | + private void deleteUser(int id) { | ||
| 298 | + int rows = jdbcTemplate.update("DELETE FROM " + TABLE_NAME + " WHERE id = ?", id); | ||
| 299 | + System.out.println("DELETE 执行影响行数:" + rows); | ||
| 300 | + if (rows != 1) { | ||
| 301 | + throw new IllegalStateException("DELETE 校验失败:预期影响 1 行,实际影响 " + rows + " 行。"); | ||
| 302 | + } | ||
| 303 | + } | ||
| 304 | + | ||
| 305 | + private Integer getUserIdByName(String name) { | ||
| 306 | + List<Integer> ids = jdbcTemplate.query("SELECT id FROM " + TABLE_NAME + " WHERE name = ? ORDER BY id DESC", | ||
| 307 | + (rs, rowNum) -> rs.getInt("id"), | ||
| 308 | + name); | ||
| 309 | + return ids.isEmpty() ? null : ids.get(0); | ||
| 310 | + } | ||
| 311 | + | ||
| 312 | + private void verifyInitialUsers() { | ||
| 313 | + List<User> users = queryUsers(); | ||
| 314 | + printUsers("初始查询", users); | ||
| 315 | + | ||
| 316 | + Set<String> existingNames = new HashSet<>(); | ||
| 317 | + for (User user : users) { | ||
| 318 | + existingNames.add(user.getName()); | ||
| 319 | + } | ||
| 320 | + for (String requiredName : REQUIRED_INITIAL_USERS) { | ||
| 321 | + if (!existingNames.contains(requiredName)) { | ||
| 322 | + throw new IllegalStateException("初始业务表校验失败:没有查到文档准备数据 " + requiredName + "。"); | ||
| 323 | + } | ||
| 324 | + } | ||
| 325 | + System.out.println("初始业务表校验通过:已查到张三、李四、王五三条基础数据。"); | ||
| 326 | + } | ||
| 327 | + | ||
| 328 | + private void selectUsers(String label) { | ||
| 329 | + List<User> users = queryUsers(); | ||
| 330 | + printUsers(label, users); | ||
| 331 | + if (users.size() < REQUIRED_INITIAL_USERS.size()) { | ||
| 332 | + throw new IllegalStateException("查询结果校验失败:预期至少 " | ||
| 333 | + + REQUIRED_INITIAL_USERS.size() + " 条基础数据,实际只有 " + users.size() + " 条。"); | ||
| 334 | + } | ||
| 335 | + System.out.println(label + "查询校验通过:基础数据仍然存在。"); | ||
| 336 | + } | ||
| 337 | + | ||
| 338 | + private List<User> queryUsers() { | ||
| 339 | + return jdbcTemplate.query("SELECT id, name, age FROM " + TABLE_NAME + " ORDER BY id", | ||
| 340 | + (rs, rowNum) -> new User(rs.getInt("id"), rs.getString("name"), rs.getInt("age"))); | ||
| 341 | + } | ||
| 342 | + | ||
| 343 | + private void printUsers(String label, List<User> users) { | ||
| 344 | + System.out.println(label + "行数:" + users.size()); | ||
| 345 | + for (User user : users) { | ||
| 346 | + System.out.println(" 用户记录:id=" + user.getId() | ||
| 347 | + + ",name=" + user.getName() | ||
| 348 | + + ",age=" + user.getAge()); | ||
| 349 | + } | ||
| 350 | + } | ||
| 351 | + | ||
| 352 | + private void assertUserExists(String name, int age) { | ||
| 353 | + Integer rows = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM " + TABLE_NAME + " WHERE name = ? AND age = ?", | ||
| 354 | + Integer.class, | ||
| 355 | + name, | ||
| 356 | + age); | ||
| 357 | + if (rows == null || rows != 1) { | ||
| 358 | + throw new IllegalStateException("用户数据校验失败:预期存在 name=" + name + " 且 age=" + age + " 的 1 行数据,实际为 " + rows + " 行。"); | ||
| 359 | + } | ||
| 360 | + System.out.println("用户数据校验通过:name=" + name + ",age=" + age + "。"); | ||
| 361 | + } | ||
| 362 | + | ||
| 363 | + private void verifyTransaction() { | ||
| 364 | + // 使用 Spring 的 TransactionTemplate,验证 Spring 事务管理器能基于 Hikari 连接正常提交事务。 | ||
| 365 | + transactionTemplate.executeWithoutResult(status -> | ||
| 366 | + jdbcTemplate.update("INSERT INTO " + TABLE_NAME + " (name, age) VALUES (?, ?)", "spring-transaction", 22)); | ||
| 367 | + | ||
| 368 | + Integer rows = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM " + TABLE_NAME + " WHERE name = ?", | ||
| 369 | + Integer.class, | ||
| 370 | + "spring-transaction"); | ||
| 371 | + if (rows == null || rows != 1) { | ||
| 372 | + throw new IllegalStateException("事务提交校验失败:预期 spring-transaction 为 1 行,实际为 " + rows + " 行。"); | ||
| 373 | + } | ||
| 374 | + | ||
| 375 | + System.out.println("事务提交校验通过:spring-transaction 数据已提交并可查询。"); | ||
| 376 | + } | ||
| 377 | + | ||
| 378 | + private void verifyPoolLimit(HikariDataSource hikariDataSource) throws Exception { | ||
| 379 | + int maxPoolSize = hikariDataSource.getMaximumPoolSize(); | ||
| 380 | + long connectionTimeoutMs = hikariDataSource.getConnectionTimeout(); | ||
| 381 | + // 观察窗口取 connectionTimeout 的一半并夹在 200ms 到 1000ms 之间, | ||
| 382 | + // 避免配置较小的 connectionTimeout 时,连接池按配置正常超时却被误判为“未进入等待队列”。 | ||
| 383 | + long observeWindowMs = Math.max(200L, Math.min(1000L, connectionTimeoutMs / 2)); | ||
| 384 | + List<Connection> borrowedConnections = new ArrayList<>(); | ||
| 385 | + ExecutorService executorService = Executors.newSingleThreadExecutor(); | ||
| 386 | + Future<Boolean> waitingConnection = null; | ||
| 387 | + | ||
| 388 | + try { | ||
| 389 | + // 先把连接池借满,模拟业务请求已经占满全部连接的场景。 | ||
| 390 | + for (int i = 0; i < maxPoolSize; i++) { | ||
| 391 | + borrowedConnections.add(dataSource.getConnection()); | ||
| 392 | + } | ||
| 393 | + | ||
| 394 | + HikariPoolMXBean poolMxBean = hikariDataSource.getHikariPoolMXBean(); | ||
| 395 | + if (poolMxBean != null) { | ||
| 396 | + System.out.println("连接池借满后活跃连接数:" + poolMxBean.getActiveConnections()); | ||
| 397 | + System.out.println("连接池借满后空闲连接数:" + poolMxBean.getIdleConnections()); | ||
| 398 | + System.out.println("连接池借满后等待线程数:" + poolMxBean.getThreadsAwaitingConnection()); | ||
| 399 | + } | ||
| 400 | + System.out.println("已借出连接数:" + borrowedConnections.size() + ",配置最大连接数:" + maxPoolSize); | ||
| 401 | + System.out.println("第 " + (maxPoolSize + 1) + " 条连接观察窗口:" + observeWindowMs | ||
| 402 | + + " ms,连接池 connectionTimeout=" + connectionTimeoutMs + " ms"); | ||
| 403 | + | ||
| 404 | + waitingConnection = executorService.submit(() -> { | ||
| 405 | + try (Connection connection = dataSource.getConnection()) { | ||
| 406 | + return connection.isValid(5); | ||
| 407 | + } | ||
| 408 | + }); | ||
| 409 | + | ||
| 410 | + Thread.sleep(observeWindowMs); | ||
| 411 | + if (waitingConnection.isDone()) { | ||
| 412 | + // isDone() 在“正常返回”与“异常结束”时均为 true,必须进一步区分, | ||
| 413 | + // 否则 CannotGetJdbcConnectionException 等异常结束会被误判为“违规放行”。 | ||
| 414 | + // 任务已结束,直接使用不带超时的 get() 取结果,避免产生不可达的超时分支。 | ||
| 415 | + try { | ||
| 416 | + waitingConnection.get(); | ||
| 417 | + // 任务正常返回:第 (maxPoolSize+1) 条连接被立即获取成功 => 校验失败 | ||
| 418 | + throw new IllegalStateException("连接池上限校验失败:第 " + (maxPoolSize + 1) | ||
| 419 | + + " 条连接在连接池已满时立即获取成功,说明 maximumPoolSize 没有形成有效限制。"); | ||
| 420 | + } catch (ExecutionException ee) { | ||
| 421 | + Throwable cause = ee.getCause(); | ||
| 422 | + if (cause instanceof SQLTransientConnectionException) { | ||
| 423 | + // 请求在等待队列中按 connectionTimeout 超时,说明连接池没有突破上限。 | ||
| 424 | + // 该请求已经结束,不再继续验证“释放连接后恢复”。 | ||
| 425 | + System.out.println("连接池上限等待校验通过:第 " + (maxPoolSize + 1) | ||
| 426 | + + " 条连接未突破最大连接数,等待 " + connectionTimeoutMs | ||
| 427 | + + " ms 后按 connectionTimeout 超时。"); | ||
| 428 | + return; | ||
| 429 | + } | ||
| 430 | + throw new IllegalStateException("连接池上限校验未达预期:第 " + (maxPoolSize + 1) | ||
| 431 | + + " 条连接请求以非等待超时异常结束(" + cause.getClass().getSimpleName() + ")。", cause); | ||
| 432 | + } | ||
| 433 | + } | ||
| 434 | + | ||
| 435 | + if (poolMxBean != null) { | ||
| 436 | + System.out.println("申请第 " + (maxPoolSize + 1) + " 条连接时等待线程数:" + poolMxBean.getThreadsAwaitingConnection()); | ||
| 437 | + } | ||
| 438 | + | ||
| 439 | + closeOneBorrowedConnection(borrowedConnections); | ||
| 440 | + Boolean connectionValid = waitingConnection.get(10, TimeUnit.SECONDS); | ||
| 441 | + if (!Boolean.TRUE.equals(connectionValid)) { | ||
| 442 | + throw new IllegalStateException("释放连接后,第 " + (maxPoolSize + 1) + " 条连接获取成功但有效性校验失败。"); | ||
| 443 | + } | ||
| 444 | + | ||
| 445 | + System.out.println("连接池上限等待校验通过:第 " + (maxPoolSize + 1) + " 条连接没有突破最大连接数,而是在等待空闲连接。"); | ||
| 446 | + | ||
| 447 | + jdbcTemplate.update("INSERT INTO " + TABLE_NAME + " (name, age) VALUES (?, ?)", "pool-limit-user", 33); | ||
| 448 | + assertUserExists("pool-limit-user", 33); | ||
| 449 | + System.out.println("连接池上限恢复校验通过:释放 1 条连接后,等待中的请求可以继续获取连接并执行 SQL。"); | ||
| 450 | + } catch (TimeoutException e) { | ||
| 451 | + throw new IllegalStateException("连接池上限校验失败:释放连接后,等待中的请求仍未在 10 秒内获取连接。", e); | ||
| 452 | + } catch (ExecutionException e) { | ||
| 453 | + throw new IllegalStateException("连接池上限校验失败:等待中的连接请求执行异常。", e.getCause()); | ||
| 454 | + } finally { | ||
| 455 | + if (waitingConnection != null && !waitingConnection.isDone()) { | ||
| 456 | + waitingConnection.cancel(true); | ||
| 457 | + } | ||
| 458 | + for (Connection connection : borrowedConnections) { | ||
| 459 | + closeQuietly(connection); | ||
| 460 | + } | ||
| 461 | + executorService.shutdownNow(); | ||
| 462 | + } | ||
| 463 | + } | ||
| 464 | + | ||
| 465 | + private void closeOneBorrowedConnection(List<Connection> borrowedConnections) { | ||
| 466 | + if (borrowedConnections.isEmpty()) { | ||
| 467 | + throw new IllegalStateException("连接池上限校验失败:没有可释放的已借出连接。"); | ||
| 468 | + } | ||
| 469 | + | ||
| 470 | + Connection connection = borrowedConnections.remove(borrowedConnections.size() - 1); | ||
| 471 | + closeQuietly(connection); | ||
| 472 | + System.out.println("已释放 1 条占用连接,用于确认等待中的连接请求可以恢复执行。"); | ||
| 473 | + } | ||
| 474 | + | ||
| 475 | + private void closeQuietly(Connection connection) { | ||
| 476 | + try { | ||
| 477 | + connection.close(); | ||
| 478 | + } catch (Exception e) { | ||
| 479 | + System.out.println("关闭连接时出现异常,继续执行清理:" + e.getMessage()); | ||
| 480 | + } | ||
| 481 | + } | ||
| 482 | + | ||
| 483 | + private void verifyConcurrentOperations() throws Exception { | ||
| 484 | + int threadCount = 10; | ||
| 485 | + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); | ||
| 486 | + // ready 确保所有 worker 已创建完毕;start 让所有 worker 同时发起连接获取。 | ||
| 487 | + CountDownLatch ready = new CountDownLatch(threadCount); | ||
| 488 | + CountDownLatch start = new CountDownLatch(1); | ||
| 489 | + // connectionsAcquired 确保 10 个 worker 都已拿到连接后,再进入真实 SQL 操作。 | ||
| 490 | + // 这样验证的是并发连接上的并发数据库操作,而不是串行借还同一个连接。 | ||
| 491 | + CountDownLatch connectionsAcquired = new CountDownLatch(threadCount); | ||
| 492 | + AtomicInteger passed = new AtomicInteger(0); | ||
| 493 | + AtomicInteger failed = new AtomicInteger(0); | ||
| 494 | + List<Future<?>> futures = new ArrayList<>(); | ||
| 495 | + | ||
| 496 | + for (int i = 0; i < threadCount; i++) { | ||
| 497 | + final int index = i; | ||
| 498 | + futures.add(executorService.submit(() -> { | ||
| 499 | + String originalName = "concurrent-user-" + index + "-" + System.nanoTime(); | ||
| 500 | + String updatedName = originalName + "-updated"; | ||
| 501 | + ready.countDown(); | ||
| 502 | + try { | ||
| 503 | + start.await(); | ||
| 504 | + try (Connection connection = dataSource.getConnection()) { | ||
| 505 | + connectionsAcquired.countDown(); | ||
| 506 | + if (!connectionsAcquired.await(10, TimeUnit.SECONDS)) { | ||
| 507 | + throw new IllegalStateException("并发连接校验失败:不是所有 worker 都能在 10 秒内拿到连接。"); | ||
| 508 | + } | ||
| 509 | + | ||
| 510 | + connection.setAutoCommit(false); | ||
| 511 | + try { | ||
| 512 | + // 每个 worker 使用不同 name,避免并发验证之间互相抢同一行。 | ||
| 513 | + int id = insertConcurrentUser(connection, originalName, 20 + index); | ||
| 514 | + assertConcurrentName(connection, id, originalName); | ||
| 515 | + updateConcurrentUser(connection, id, updatedName, 30 + index); | ||
| 516 | + assertConcurrentName(connection, id, updatedName); | ||
| 517 | + deleteConcurrentUser(connection, id); | ||
| 518 | + connection.commit(); | ||
| 519 | + passed.incrementAndGet(); | ||
| 520 | + } catch (Exception e) { | ||
| 521 | + connection.rollback(); | ||
| 522 | + throw e; | ||
| 523 | + } finally { | ||
| 524 | + connection.setAutoCommit(true); | ||
| 525 | + } | ||
| 526 | + } | ||
| 527 | + } catch (Exception e) { | ||
| 528 | + failed.incrementAndGet(); | ||
| 529 | + throw new IllegalStateException("第 " + index + " 个并发 worker 执行数据库操作失败。", e); | ||
| 530 | + } | ||
| 531 | + })); | ||
| 532 | + } | ||
| 533 | + | ||
| 534 | + try { | ||
| 535 | + if (!ready.await(10, TimeUnit.SECONDS)) { | ||
| 536 | + throw new IllegalStateException("并发 worker 启动超时:10 秒内没有全部准备完成。"); | ||
| 537 | + } | ||
| 538 | + } catch (InterruptedException e) { | ||
| 539 | + Thread.currentThread().interrupt(); | ||
| 540 | + throw new IllegalStateException("并发 worker 启动被中断。", e); | ||
| 541 | + } finally { | ||
| 542 | + if (!ready.await(0, TimeUnit.SECONDS)) { | ||
| 543 | + executorService.shutdownNow(); | ||
| 544 | + } | ||
| 545 | + } | ||
| 546 | + | ||
| 547 | + start.countDown(); | ||
| 548 | + try { | ||
| 549 | + for (Future<?> future : futures) { | ||
| 550 | + try { | ||
| 551 | + future.get(30, TimeUnit.SECONDS); | ||
| 552 | + } catch (ExecutionException e) { | ||
| 553 | + throw new IllegalStateException("并发数据库操作校验失败:成功 " + passed.get() | ||
| 554 | + + " 个,失败 " + failed.get() + " 个。", e.getCause()); | ||
| 555 | + } | ||
| 556 | + } | ||
| 557 | + | ||
| 558 | + // 所有 worker 正常完成,passed 应等于 threadCount | ||
| 559 | + // 无需额外检查,直接打印通过信息 | ||
| 560 | + | ||
| 561 | + | ||
| 562 | + System.out.println("并发真实数据库操作校验通过:" + passed.get() | ||
| 563 | + + "/" + threadCount + " 个 worker 均完成 insert/select/update/select/delete/commit。"); | ||
| 564 | +} catch (TimeoutException e) { | ||
| 565 | + throw new IllegalStateException("并发数据库操作校验超时:存在 worker 在 30 秒内没有完成真实 SQL 操作。", e); | ||
| 566 | +} finally { | ||
| 567 | + executorService.shutdownNow(); | ||
| 568 | + if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) { | ||
| 569 | + System.out.println("并发线程池未能在 10 秒内完全停止,程序继续退出。"); | ||
| 570 | + } | ||
| 571 | + } | ||
| 572 | + } | ||
| 573 | + | ||
| 574 | + private int insertConcurrentUser(Connection connection, String name, int age) throws Exception { | ||
| 575 | + // 先尝试 JDBC generated keys;如果 dolphin/驱动组合没有返回 generated key, | ||
| 576 | + // 再按唯一 name 查询 id,保证验证能覆盖更多兼容实现。 | ||
| 577 | + try (PreparedStatement statement = connection.prepareStatement( | ||
| 578 | + "INSERT INTO " + TABLE_NAME + " (name, age) VALUES (?, ?)", | ||
| 579 | + PreparedStatement.RETURN_GENERATED_KEYS)) { | ||
| 580 | + statement.setString(1, name); | ||
| 581 | + statement.setInt(2, age); | ||
| 582 | + int rows = statement.executeUpdate(); | ||
| 583 | + if (rows != 1) { | ||
| 584 | + throw new IllegalStateException("并发 INSERT 校验失败:预期影响 1 行,实际影响 " + rows + " 行。"); | ||
| 585 | + } | ||
| 586 | + | ||
| 587 | + try (ResultSet keys = statement.getGeneratedKeys()) { | ||
| 588 | + if (keys.next()) { | ||
| 589 | + return keys.getInt(1); | ||
| 590 | + } | ||
| 591 | + } | ||
| 592 | + } | ||
| 593 | + | ||
| 594 | + try (PreparedStatement statement = connection.prepareStatement( | ||
| 595 | + "SELECT id FROM " + TABLE_NAME + " WHERE name = ?")) { | ||
| 596 | + statement.setString(1, name); | ||
| 597 | + try (ResultSet rs = statement.executeQuery()) { | ||
| 598 | + if (rs.next()) { | ||
| 599 | + return rs.getInt("id"); | ||
| 600 | + } | ||
| 601 | + } | ||
| 602 | + } | ||
| 603 | + | ||
| 604 | + throw new IllegalStateException("并发 INSERT 后没有查到新数据 id,name=" + name); | ||
| 605 | + } | ||
| 606 | + | ||
| 607 | + private void updateConcurrentUser(Connection connection, int id, String name, int age) throws Exception { | ||
| 608 | + try (PreparedStatement statement = connection.prepareStatement( | ||
| 609 | + "UPDATE " + TABLE_NAME + " SET name = ?, age = ? WHERE id = ?")) { | ||
| 610 | + statement.setString(1, name); | ||
| 611 | + statement.setInt(2, age); | ||
| 612 | + statement.setInt(3, id); | ||
| 613 | + int rows = statement.executeUpdate(); | ||
| 614 | + if (rows != 1) { | ||
| 615 | + throw new IllegalStateException("并发 UPDATE 校验失败:预期影响 1 行,实际影响 " + rows + " 行。"); | ||
| 616 | + } | ||
| 617 | + } | ||
| 618 | + } | ||
| 619 | + | ||
| 620 | + private void assertConcurrentName(Connection connection, int id, String expectedName) throws Exception { | ||
| 621 | + try (PreparedStatement statement = connection.prepareStatement( | ||
| 622 | + "SELECT name FROM " + TABLE_NAME + " WHERE id = ?")) { | ||
| 623 | + statement.setInt(1, id); | ||
| 624 | + try (ResultSet rs = statement.executeQuery()) { | ||
| 625 | + if (!rs.next()) { | ||
| 626 | + throw new IllegalStateException("并发 SELECT 校验失败:没有查到 id=" + id + " 的数据。"); | ||
| 627 | + } | ||
| 628 | + String actualName = rs.getString("name"); | ||
| 629 | + if (!expectedName.equals(actualName)) { | ||
| 630 | + throw new IllegalStateException("并发 SELECT 校验失败:预期 name=" + expectedName + ",实际 name=" + actualName + "。"); | ||
| 631 | + } | ||
| 632 | + } | ||
| 633 | + } | ||
| 634 | + } | ||
| 635 | + | ||
| 636 | + private void deleteConcurrentUser(Connection connection, int id) throws Exception { | ||
| 637 | + try (PreparedStatement statement = connection.prepareStatement( | ||
| 638 | + "DELETE FROM " + TABLE_NAME + " WHERE id = ?")) { | ||
| 639 | + statement.setInt(1, id); | ||
| 640 | + int rows = statement.executeUpdate(); | ||
| 641 | + if (rows != 1) { | ||
| 642 | + throw new IllegalStateException("并发 DELETE 校验失败:预期影响 1 行,实际影响 " + rows + " 行。"); | ||
| 643 | + } | ||
| 644 | + } | ||
| 645 | + } | ||
| 646 | +} | ||
| @@ -0,0 +1,18 @@ | |||
| 1 | +package com.linyu.ospp; | ||
| 2 | + | ||
| 3 | +import org.springframework.boot.SpringApplication; | ||
| 4 | +import org.springframework.boot.autoconfigure.SpringBootApplication; | ||
| 5 | + | ||
| 6 | +import java.io.PrintStream; | ||
| 7 | +import java.nio.charset.StandardCharsets; | ||
| 8 | + | ||
| 9 | + | ||
| 10 | +public class SpringBootOsppApplication { | ||
| 11 | + | ||
| 12 | + public static void main(String[] args) throws Exception { | ||
| 13 | + System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8.name())); | ||
| 14 | + System.setErr(new PrintStream(System.err, true, StandardCharsets.UTF_8.name())); | ||
| 15 | + SpringApplication.run(SpringBootOsppApplication.class, args); | ||
| 16 | + } | ||
| 17 | + | ||
| 18 | +} | ||
| @@ -0,0 +1,743 @@ | |||
| 1 | +package com.linyu.ospp; | ||
| 2 | + | ||
| 3 | +import com.zaxxer.hikari.HikariConfig; | ||
| 4 | +import com.zaxxer.hikari.HikariDataSource; | ||
| 5 | + | ||
| 6 | +import java.sql.Connection; | ||
| 7 | +import java.sql.PreparedStatement; | ||
| 8 | +import java.sql.ResultSet; | ||
| 9 | +import java.sql.SQLException; | ||
| 10 | +import java.sql.Statement; | ||
| 11 | +import java.util.ArrayList; | ||
| 12 | +import java.util.List; | ||
| 13 | +import java.util.concurrent.CountDownLatch; | ||
| 14 | +import java.util.concurrent.ExecutorService; | ||
| 15 | +import java.util.concurrent.Executors; | ||
| 16 | +import java.util.concurrent.TimeUnit; | ||
| 17 | +import java.util.concurrent.atomic.AtomicInteger; | ||
| 18 | + | ||
| 19 | +/** | ||
| 20 | + * 五类标准 SQL 操作验证程序(DDL / DML / DQL / DCL / TCL)+ 并发连接测试。 | ||
| 21 | + * 通过 HikariCP 连接池 + MySQL Connector/J 驱动,经 dolphin MySQL 协议连接 openGauss B 兼容库。 | ||
| 22 | + * 运行方式:java -cp "hikaricp-5.1.0.jar:mysql-connector-java-8.0.20.jar:." com.linyu.ospp.SqlCategoryVerificationRunner | ||
| 23 | + * 或在 Spring Boot 项目中通过 SpringApplication.run 启动后自动执行。 | ||
| 24 | + */ | ||
| 25 | +public class SqlCategoryVerificationRunner { | ||
| 26 | + | ||
| 27 | + // 连接参数:优先从环境变量读取,避免明文口令与内部 IP 进入版本库;缺失口令时显式报错。 | ||
| 28 | + private static final String DB_HOST = System.getenv().getOrDefault("OPENGAUSS_MYSQL_HOST", "127.0.0.1"); | ||
| 29 | + private static final String DB_PORT = System.getenv().getOrDefault("OPENGAUSS_MYSQL_PORT", "3306"); | ||
| 30 | + private static final String DB_NAME = System.getenv().getOrDefault("OPENGAUSS_MYSQL_DB", "mysql_db"); | ||
| 31 | + private static final String USERNAME = System.getenv().getOrDefault("OPENGAUSS_MYSQL_USER", "mysqluser"); | ||
| 32 | + private static final String PASSWORD = requireEnv("OPENGAUSS_MYSQL_PASSWORD"); | ||
| 33 | + private static final String BASE_URL = "jdbc:mysql://" + DB_HOST + ":" + DB_PORT + "/"; | ||
| 34 | + private static final String JDBC_URL = BASE_URL + DB_NAME + "?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"; | ||
| 35 | + | ||
| 36 | + // 使用独立表名,避免与既有 user 表冲突 | ||
| 37 | + private static final String TEST_TABLE = "sql_category_test"; | ||
| 38 | + private static final String TEST_TABLE_2 = "sql_category_test_2"; | ||
| 39 | + | ||
| 40 | + // 结论表列宽,中文按两列宽度计算 | ||
| 41 | + private static final int CATEGORY_WIDTH = 8; | ||
| 42 | + private static final int OPERATION_WIDTH = 29; | ||
| 43 | + private static final int RESULT_WIDTH = 26; | ||
| 44 | + | ||
| 45 | + private static int sectionNo = 1; | ||
| 46 | + | ||
| 47 | + // 结论表按实际执行结果统计,避免写死 PASS / WARN 数量与实测输出不一致。 | ||
| 48 | + // DCL 的 GRANT / REVOKE 在权限不足时记 WARN,因此单独计数。 | ||
| 49 | + private static int dclPassCount = 0; | ||
| 50 | + private static int dclWarnCount = 0; | ||
| 51 | + private static int dclInfoCount = 0; | ||
| 52 | + // DML-05 是否取到自增主键,决定该项记 PASS 还是 INFO。 | ||
| 53 | + private static boolean generatedKeysReturned = false; | ||
| 54 | + | ||
| 55 | + public static void main(String[] args) throws Exception { | ||
| 56 | + System.out.println("========================================"); | ||
| 57 | + System.out.println(" openGauss B 兼容模式 五类 SQL 操作验证"); | ||
| 58 | + System.out.println(" 驱动:MySQL Connector/J 8.0.20"); | ||
| 59 | + System.out.println(" 连接池:HikariCP"); | ||
| 60 | + System.out.println(" 协议:dolphin MySQL 协议 (3306)"); | ||
| 61 | + System.out.println(" 目标:openGauss 7.0.0-RC3 + dolphin 5.2"); | ||
| 62 | + System.out.println("========================================"); | ||
| 63 | + | ||
| 64 | + // 先确保目标数据库存在(连接到默认库创建 schema) | ||
| 65 | + ensureDatabaseExists(); | ||
| 66 | + | ||
| 67 | + HikariDataSource dataSource = initDataSource(); | ||
| 68 | + | ||
| 69 | + try { | ||
| 70 | + runAllTests(dataSource); | ||
| 71 | + } finally { | ||
| 72 | + dataSource.close(); | ||
| 73 | + System.out.println(); | ||
| 74 | + System.out.println("连接池已关闭。全部验证结束。"); | ||
| 75 | + } | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + private static HikariDataSource initDataSource() { | ||
| 79 | + printSection("连接池初始化"); | ||
| 80 | + HikariConfig config = new HikariConfig(); | ||
| 81 | + config.setJdbcUrl(JDBC_URL); | ||
| 82 | + config.setUsername(USERNAME); | ||
| 83 | + config.setPassword(PASSWORD); | ||
| 84 | + config.setDriverClassName("com.mysql.cj.jdbc.Driver"); | ||
| 85 | + config.setMaximumPoolSize(10); | ||
| 86 | + config.setMinimumIdle(2); | ||
| 87 | + config.setConnectionTimeout(30000); | ||
| 88 | + config.setIdleTimeout(600000); | ||
| 89 | + config.setMaxLifetime(1800000); | ||
| 90 | + config.setPoolName("SqlCategoryPool"); | ||
| 91 | + HikariDataSource ds = new HikariDataSource(config); | ||
| 92 | + System.out.println("HikariCP 连接池初始化完成:poolName=" + ds.getPoolName() | ||
| 93 | + + ", maximumPoolSize=" + ds.getMaximumPoolSize()); | ||
| 94 | + return ds; | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + private static void ensureDatabaseExists() throws Exception { | ||
| 98 | + printSection("确保目标数据库存在"); | ||
| 99 | + // 先连接到 postgres(openGauss B 兼容库默认库)创建目标 schema | ||
| 100 | + HikariConfig initCfg = new HikariConfig(); | ||
| 101 | + initCfg.setJdbcUrl(BASE_URL + "postgres?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"); | ||
| 102 | + initCfg.setUsername(USERNAME); | ||
| 103 | + initCfg.setPassword(PASSWORD); | ||
| 104 | + initCfg.setDriverClassName("com.mysql.cj.jdbc.Driver"); | ||
| 105 | + initCfg.setPoolName("InitPool"); | ||
| 106 | + try (HikariDataSource initDs = new HikariDataSource(initCfg); | ||
| 107 | + Connection conn = initDs.getConnection(); | ||
| 108 | + Statement stmt = conn.createStatement()) { | ||
| 109 | + // 检查 schema 是否已存在 | ||
| 110 | + ResultSet rs = stmt.executeQuery("SELECT 1 FROM pg_namespace WHERE nspname = '" + DB_NAME + "'"); | ||
| 111 | + if (rs.next()) { | ||
| 112 | + System.out.println("数据库 " + DB_NAME + " 已存在,跳过创建。"); | ||
| 113 | + } else { | ||
| 114 | + stmt.executeUpdate("CREATE SCHEMA " + DB_NAME); | ||
| 115 | + System.out.println("已创建数据库(schema):" + DB_NAME); | ||
| 116 | + } | ||
| 117 | + } | ||
| 118 | + } | ||
| 119 | + | ||
| 120 | + private static void runAllTests(HikariDataSource dataSource) throws Exception { | ||
| 121 | + // ====== 1. DDL 验证 ====== | ||
| 122 | + verifyDDL(dataSource); | ||
| 123 | + | ||
| 124 | + // ====== 2. DML 验证 ====== | ||
| 125 | + verifyDML(dataSource); | ||
| 126 | + | ||
| 127 | + // ====== 3. DQL 验证 ====== | ||
| 128 | + verifyDQL(dataSource); | ||
| 129 | + | ||
| 130 | + // ====== 4. DCL 验证 ====== | ||
| 131 | + verifyDCL(dataSource); | ||
| 132 | + | ||
| 133 | + // ====== 5. TCL 验证 ====== | ||
| 134 | + verifyTCL(dataSource); | ||
| 135 | + | ||
| 136 | + // ====== 6. 并发连接验证 ====== | ||
| 137 | + verifyConcurrentConnections(dataSource); | ||
| 138 | + | ||
| 139 | + // ====== 清理 DDL 残留表 ====== | ||
| 140 | + cleanupTables(dataSource); | ||
| 141 | + | ||
| 142 | + // ====== 最终结论 ====== | ||
| 143 | + printFinalConclusion(); | ||
| 144 | + } | ||
| 145 | + | ||
| 146 | + // ==================== DDL ==================== | ||
| 147 | + private static void verifyDDL(HikariDataSource dataSource) throws Exception { | ||
| 148 | + printSection("DDL(Data Definition Language)验证"); | ||
| 149 | + printPurpose("验证 CREATE / ALTER / DROP / TRUNCATE 四种 DDL 操作通过 dolphin MySQL 协议正常执行。"); | ||
| 150 | + | ||
| 151 | + try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement()) { | ||
| 152 | + | ||
| 153 | + // --- CREATE TABLE --- | ||
| 154 | + System.out.println("[DDL-01] CREATE TABLE - 创建主测试表"); | ||
| 155 | + stmt.executeUpdate("DROP TABLE IF EXISTS " + TEST_TABLE); | ||
| 156 | + String createSql = "CREATE TABLE " + TEST_TABLE + " (" | ||
| 157 | + + "id SERIAL PRIMARY KEY, " | ||
| 158 | + + "name VARCHAR(64) NOT NULL, " | ||
| 159 | + + "age INT DEFAULT 0, " | ||
| 160 | + + "score DECIMAL(5,2), " | ||
| 161 | + + "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP" | ||
| 162 | + + ")"; | ||
| 163 | + stmt.executeUpdate(createSql); | ||
| 164 | + System.out.println(" PASS | CREATE TABLE " + TEST_TABLE + " 执行成功(含 SERIAL 主键、VARCHAR、INT、DECIMAL、TIMESTAMP 字段)"); | ||
| 165 | + | ||
| 166 | + // --- CREATE INDEX --- | ||
| 167 | + System.out.println("[DDL-02] CREATE INDEX - 创建索引"); | ||
| 168 | + stmt.executeUpdate("CREATE INDEX idx_" + TEST_TABLE + "_name ON " + TEST_TABLE + " (name)"); | ||
| 169 | + System.out.println(" PASS | CREATE INDEX idx_" + TEST_TABLE + "_name 执行成功"); | ||
| 170 | + | ||
| 171 | + // --- ALTER TABLE ADD COLUMN --- | ||
| 172 | + System.out.println("[DDL-03] ALTER TABLE ADD COLUMN - 增加列"); | ||
| 173 | + stmt.executeUpdate("ALTER TABLE " + TEST_TABLE + " ADD COLUMN remark TEXT"); | ||
| 174 | + System.out.println(" PASS | ALTER TABLE ... ADD COLUMN remark TEXT 执行成功"); | ||
| 175 | + | ||
| 176 | + // --- ALTER TABLE RENAME COLUMN --- | ||
| 177 | + System.out.println("[DDL-04] ALTER TABLE RENAME COLUMN - 重命名列"); | ||
| 178 | + stmt.executeUpdate("ALTER TABLE " + TEST_TABLE + " RENAME COLUMN remark TO description"); | ||
| 179 | + System.out.println(" PASS | ALTER TABLE ... RENAME COLUMN remark TO description 执行成功"); | ||
| 180 | + | ||
| 181 | + // --- TRUNCATE TABLE(先插入数据再清空)--- | ||
| 182 | + stmt.executeUpdate("INSERT INTO " + TEST_TABLE + " (name, age, score) VALUES ('truncate_test', 99, 100.00)"); | ||
| 183 | + System.out.println("[DDL-05] TRUNCATE TABLE - 清空表数据"); | ||
| 184 | + stmt.executeUpdate("TRUNCATE TABLE " + TEST_TABLE); | ||
| 185 | + int countAfterTruncate = queryCount(conn, TEST_TABLE); | ||
| 186 | + if (countAfterTruncate == 0) { | ||
| 187 | + System.out.println(" PASS | TRUNCATE TABLE " + TEST_TABLE + " 执行成功,当前行数=" + countAfterTruncate); | ||
| 188 | + } else { | ||
| 189 | + throw new IllegalStateException("TRUNCATE 失败:预期 0 行,实际 " + countAfterTruncate + " 行"); | ||
| 190 | + } | ||
| 191 | + | ||
| 192 | + // --- DROP TABLE --- | ||
| 193 | + System.out.println("[DDL-06] DROP TABLE - 删除第二张测试表(先建再删)"); | ||
| 194 | + stmt.executeUpdate("CREATE TABLE " + TEST_TABLE_2 + " (id INT PRIMARY KEY, val VARCHAR(32))"); | ||
| 195 | + stmt.executeUpdate("DROP TABLE " + TEST_TABLE_2); | ||
| 196 | + System.out.println(" PASS | DROP TABLE " + TEST_TABLE_2 + " 执行成功"); | ||
| 197 | + | ||
| 198 | + // --- CREATE TABLE AS SELECT --- | ||
| 199 | + System.out.println("[DDL-07] CREATE TABLE AS SELECT - 从查询结果创建表"); | ||
| 200 | + stmt.executeUpdate("INSERT INTO " + TEST_TABLE + " (name, age, score) VALUES ('ctas_src', 25, 88.50)"); | ||
| 201 | + stmt.executeUpdate("CREATE TABLE " + TEST_TABLE_2 + " AS SELECT name, age FROM " + TEST_TABLE + " WHERE name = 'ctas_src'"); | ||
| 202 | + int ctasCount = queryCount(conn, TEST_TABLE_2); | ||
| 203 | + System.out.println(" PASS | CREATE TABLE " + TEST_TABLE_2 + " AS SELECT 执行成功,目标表行数=" + ctasCount); | ||
| 204 | + stmt.executeUpdate("DROP TABLE " + TEST_TABLE_2); | ||
| 205 | + } | ||
| 206 | + | ||
| 207 | + System.out.println("=> DDL 全部 7 项操作验证通过(CREATE/INDEX/ALTER ADD/RENAME/TRUNCATE/DROP/CTAS)。"); | ||
| 208 | + } | ||
| 209 | + | ||
| 210 | + // ==================== DML ==================== | ||
| 211 | + private static void verifyDML(HikariDataSource dataSource) throws Exception { | ||
| 212 | + printSection("DML(Data Manipulation Language)验证"); | ||
| 213 | + printPurpose("验证 INSERT / UPDATE / DELETE 三种 DML 操作及批量写入通过 dolphin MySQL 协议正常执行。"); | ||
| 214 | + | ||
| 215 | + try (Connection conn = dataSource.getConnection()) { | ||
| 216 | + | ||
| 217 | + // --- INSERT 单行 --- | ||
| 218 | + System.out.println("[DML-01] INSERT - 单行插入"); | ||
| 219 | + try (PreparedStatement ps = conn.prepareStatement( | ||
| 220 | + "INSERT INTO " + TEST_TABLE + " (name, age, score) VALUES (?, ?, ?)")) { | ||
| 221 | + ps.setString(1, "Alice"); | ||
| 222 | + ps.setInt(2, 28); | ||
| 223 | + ps.setDouble(3, 95.50); | ||
| 224 | + int rows = ps.executeUpdate(); | ||
| 225 | + System.out.println(" PASS | INSERT 单行影响行数=" + rows + " (name=Alice, age=28, score=95.50)"); | ||
| 226 | + } | ||
| 227 | + | ||
| 228 | + // --- INSERT 多行 --- | ||
| 229 | + System.out.println("[DML-02] INSERT - 多行插入"); | ||
| 230 | + try (PreparedStatement ps = conn.prepareStatement( | ||
| 231 | + "INSERT INTO " + TEST_TABLE + " (name, age, score) VALUES (?, ?, ?)")) { | ||
| 232 | + for (int i = 0; i < 3; i++) { | ||
| 233 | + ps.setString(1, "BatchUser" + i); | ||
| 234 | + ps.setInt(2, 20 + i); | ||
| 235 | + ps.setDouble(3, 80.0 + i * 5); | ||
| 236 | + ps.addBatch(); | ||
| 237 | + } | ||
| 238 | + int[] results = ps.executeBatch(); | ||
| 239 | + int total = 0; | ||
| 240 | + for (int r : results) total += r; | ||
| 241 | + System.out.println(" PASS | INSERT 批量 3 行影响总行数=" + total); | ||
| 242 | + } | ||
| 243 | + | ||
| 244 | + // --- UPDATE --- | ||
| 245 | + System.out.println("[DML-03] UPDATE - 条件更新"); | ||
| 246 | + try (PreparedStatement ps = conn.prepareStatement( | ||
| 247 | + "UPDATE " + TEST_TABLE + " SET age = ?, score = ? WHERE name = ?")) { | ||
| 248 | + ps.setInt(1, 29); | ||
| 249 | + ps.setDouble(2, 96.00); | ||
| 250 | + ps.setString(3, "Alice"); | ||
| 251 | + int rows = ps.executeUpdate(); | ||
| 252 | + System.out.println(" PASS | UPDATE 影响行数=" + rows + " (Alice age:28->29, score:95.50->96.00)"); | ||
| 253 | + | ||
| 254 | + // 验证更新结果 | ||
| 255 | + try (PreparedStatement qs = conn.prepareStatement( | ||
| 256 | + "SELECT age, score FROM " + TEST_TABLE + " WHERE name = 'Alice'")) { | ||
| 257 | + ResultSet rs = qs.executeQuery(); | ||
| 258 | + if (rs.next()) { | ||
| 259 | + int newAge = rs.getInt("age"); | ||
| 260 | + double newScore = rs.getDouble("score"); | ||
| 261 | + System.out.println(" 确认 | 更新后 Alice age=" + newAge + ", score=" + newScore); | ||
| 262 | + } | ||
| 263 | + } | ||
| 264 | + } | ||
| 265 | + | ||
| 266 | + // --- DELETE --- | ||
| 267 | + System.out.println("[DML-04] DELETE - 条件删除"); | ||
| 268 | + try (PreparedStatement ps = conn.prepareStatement( | ||
| 269 | + "DELETE FROM " + TEST_TABLE + " WHERE name LIKE 'BatchUser%'")) { | ||
| 270 | + int rows = ps.executeUpdate(); | ||
| 271 | + System.out.println(" PASS | DELETE 影响行数=" + rows + " (删除所有 BatchUser*)"); | ||
| 272 | + } | ||
| 273 | + | ||
| 274 | + // --- 获取自增主键(dolphin 兼容):JDBC getGeneratedKeys --- | ||
| 275 | + System.out.println("[DML-05] JDBC getGeneratedKeys - 获取自增主键(dolphin 兼容)"); | ||
| 276 | + try (PreparedStatement ps = conn.prepareStatement( | ||
| 277 | + "INSERT INTO " + TEST_TABLE + " (name, age, score) VALUES ('Bob', 35, 87.25)", | ||
| 278 | + PreparedStatement.RETURN_GENERATED_KEYS)) { | ||
🔵 Low Priority 第 258 行将 DML-05 标注为 "INSERT ... RETURNING - 插入并返回生成列",PR 描述也宣称 DML 覆盖 "INSERT RETURNING(getGeneratedKeys)",但第 259-261 行实际执行的是普通 INSERT 并附带 PreparedStatement.RETURN_GENERATED_KEYS 标志,通过 JDBC getGeneratedKeys() 取自增主键——全程未执行任何 INSERT ... RETURNING SQL 语句。INSERT...RETURNING 是 dolphin/openGauss 的 SQL 扩展兼容点,而 getGeneratedKeys 是 JDBC 驱动的标准机制,二者是不同的验证对象。因此该验证项并不能证明 dolphin 对 INSERT ... RETURNING 语法的兼容性,28 项证据中该项名不副实。修复方向:若确要验证 RETURNING 兼容,应改用 "INSERT INTO ... VALUES (...) RETURNING id" 的真实 SQL 并读取结果集;否则应将该验证项更名为"JDBC getGeneratedKeys 获取自增主键",避免误导。 建议:将 SQL 改为真实的 "INSERT INTO ... VALUES (...) RETURNING id" 并解析结果集以验证 dolphin 兼容性;或把该验证项更名为"JDBC getGeneratedKeys 获取自增主键",与代码实际行为一致。 ![]() ![]() | |||
| 279 | + ps.executeUpdate(); | ||
| 280 | + ResultSet keys = ps.getGeneratedKeys(); | ||
| 281 | + if (keys.next()) { | ||
| 282 | + long genId = keys.getLong(1); | ||
| 283 | + generatedKeysReturned = true; | ||
| 284 | + System.out.println(" PASS | INSERT Bob 后获取到自增 id=" + genId); | ||
| 285 | + } else { | ||
| 286 | + System.out.println(" INFO | 驱动未返回 GENERATED_KEYS(不影响插入成功)"); | ||
| 287 | + } | ||
| 288 | + } | ||
| 289 | + } | ||
| 290 | + | ||
| 291 | + if (generatedKeysReturned) { | ||
| 292 | + System.out.println("=> DML 全部 5 项操作验证通过(单行INSERT/批量INSERT/UPDATE/DELETE/getGeneratedKeys)。"); | ||
| 293 | + } else { | ||
| 294 | + System.out.println("=> DML 共 4 项操作验证通过(单行INSERT/批量INSERT/UPDATE/DELETE);" | ||
| 295 | + + "DML-05 getGeneratedKeys 为 INFO,不计入 PASS。"); | ||
| 296 | + } | ||
| 297 | + } | ||
| 298 | + | ||
| 299 | + // ==================== DQL ==================== | ||
| 300 | + private static void verifyDQL(HikariDataSource dataSource) throws Exception { | ||
| 301 | + printSection("DQL(Data Query Language)验证"); | ||
| 302 | + printPurpose("验证 SELECT 及其子句(WHERE / ORDER BY / GROUP BY / 聚合函数 / LIMIT / JOIN)通过 dolphin MySQL 协议正常执行。"); | ||
| 303 | + | ||
| 304 | + try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement()) { | ||
| 305 | + | ||
| 306 | + // 准备查询数据 | ||
| 307 | + stmt.executeUpdate("INSERT INTO " + TEST_TABLE + " (name, age, score) VALUES ('Charlie', 22, 78.00)"); | ||
| 308 | + stmt.executeUpdate("INSERT INTO " + TEST_TABLE + " (name, age, score) VALUES ('Diana', 31, 92.50)"); | ||
| 309 | + stmt.executeUpdate("INSERT INTO " + TEST_TABLE + " (name, age, score) VALUES ('Eve', 27, 85.75)"); | ||
| 310 | + | ||
| 311 | + // --- 基础 SELECT * --- | ||
| 312 | + System.out.println("[DQL-01] SELECT * - 全量查询"); | ||
| 313 | + List<String> allRows = new ArrayList<>(); | ||
| 314 | + try (ResultSet rs = stmt.executeQuery("SELECT id, name, age, score FROM " + TEST_TABLE + " ORDER BY id")) { | ||
| 315 | + while (rs.next()) { | ||
| 316 | + allRows.add(rs.getInt("id") + "|" + rs.getString("name") + "|" + rs.getInt("age") + "|" + rs.getDouble("score")); | ||
| 317 | + } | ||
| 318 | + } | ||
| 319 | + System.out.println(" PASS | SELECT * 返回 " + allRows.size() + " 行"); | ||
| 320 | + for (String row : allRows) { | ||
| 321 | + System.out.println(" " + row); | ||
| 322 | + } | ||
| 323 | + | ||
| 324 | + // --- SELECT WHERE --- | ||
| 325 | + System.out.println("[DQL-02] SELECT ... WHERE - 条件过滤"); | ||
| 326 | + try (ResultSet rs = stmt.executeQuery( | ||
| 327 | + "SELECT name, age FROM " + TEST_TABLE + " WHERE age >= 28 ORDER BY age")) { | ||
| 328 | + List<String> filtered = new ArrayList<>(); | ||
| 329 | + while (rs.next()) { | ||
| 330 | + filtered.add(rs.getString("name") + "(age=" + rs.getInt("age") + ")"); | ||
| 331 | + } | ||
| 332 | + System.out.println(" PASS | WHERE age>=28 返回 " + filtered.size() + " 行:" + String.join(", ", filtered)); | ||
| 333 | + } | ||
| 334 | + | ||
| 335 | + // --- SELECT ORDER BY + LIMIT --- | ||
| 336 | + System.out.println("[DQL-03] SELECT ... ORDER BY + LIMIT - 排序与分页"); | ||
| 337 | + try (ResultSet rs = stmt.executeQuery( | ||
| 338 | + "SELECT name, score FROM " + TEST_TABLE + " ORDER BY score DESC LIMIT 3")) { | ||
| 339 | + List<String> top3 = new ArrayList<>(); | ||
| 340 | + while (rs.next()) { | ||
| 341 | + top3.add(rs.getString("name") + "(" + rs.getDouble("score") + ")"); | ||
| 342 | + } | ||
| 343 | + System.out.println(" PASS | TOP 3 by score DESC:" + String.join(" > ", top3)); | ||
| 344 | + } | ||
| 345 | + | ||
| 346 | + // --- 聚合函数 COUNT/SUM/AVG/MIN/MAX --- | ||
| 347 | + System.out.println("[DQL-04] 聚合函数 - COUNT / SUM / AVG / MIN / MAX"); | ||
| 348 | + try (ResultSet rs = stmt.executeQuery( | ||
| 349 | + "SELECT COUNT(*) AS cnt, SUM(age) AS sum_age, AVG(score) AS avg_score, " | ||
| 350 | + + "MIN(age) AS min_age, MAX(age) AS max_age FROM " + TEST_TABLE)) { | ||
| 351 | + if (rs.next()) { | ||
| 352 | + System.out.println(" PASS | COUNT=" + rs.getInt("cnt") | ||
| 353 | + + ", SUM(age)=" + rs.getLong("sum_age") | ||
| 354 | + + ", AVG(score)=" + Math.round(rs.getDouble("avg_score") * 100.0) / 100.0 | ||
| 355 | + + ", MIN(age)=" + rs.getInt("min_age") | ||
| 356 | + + ", MAX(age)=" + rs.getInt("max_age")); | ||
| 357 | + } | ||
| 358 | + } | ||
| 359 | + | ||
| 360 | + // --- GROUP BY + HAVING --- | ||
| 361 | + System.out.println("[DQL-05] GROUP BY + HAVING - 分组聚合与过滤"); | ||
| 362 | + try (ResultSet rs = stmt.executeQuery( | ||
| 363 | + "SELECT CASE WHEN age < 25 THEN 'young' ELSE 'senior' END AS group_name, " | ||
| 364 | + + "COUNT(*) AS cnt, AVG(score) AS avg_s " | ||
| 365 | + + "FROM " + TEST_TABLE + " GROUP BY CASE WHEN age < 25 THEN 'young' ELSE 'senior' END " | ||
| 366 | + + "HAVING COUNT(*) >= 1")) { | ||
| 367 | + List<String> groups = new ArrayList<>(); | ||
| 368 | + while (rs.next()) { | ||
| 369 | + groups.add(rs.getString("group_name") + ": cnt=" + rs.getInt("cnt") + ", avg_score=" | ||
| 370 | + + Math.round(rs.getDouble("avg_s") * 100.0) / 100.0); | ||
| 371 | + } | ||
| 372 | + System.out.println(" PASS | GROUP BY 分组数=" + groups.size() + ":" + String.join("; ", groups)); | ||
| 373 | + } | ||
| 374 | + | ||
| 375 | + // --- DISTINCT --- | ||
| 376 | + System.out.println("[DQL-06] SELECT DISTINCT - 去重"); | ||
| 377 | + stmt.executeUpdate("INSERT INTO " + TEST_TABLE + " (name, age, score) VALUES ('Alice', 40, 70.00)"); | ||
| 378 | + try (ResultSet rs = stmt.executeQuery("SELECT DISTINCT name FROM " + TEST_TABLE + " ORDER BY name")) { | ||
| 379 | + List<String> names = new ArrayList<>(); | ||
| 380 | + while (rs.next()) names.add(rs.getString("name")); | ||
| 381 | + System.out.println(" PASS | DISTINCT name 返回 " + names.size() + " 个唯一值:" + String.join(", ", names)); | ||
| 382 | + } | ||
| 383 | + | ||
| 384 | + // 清理 DQL-06 为演示 DISTINCT 额外插入的重复行(name='Alice', age=40),避免污染后续子查询 | ||
| 385 | + stmt.executeUpdate("DELETE FROM " + TEST_TABLE + " WHERE name = 'Alice' AND age = 40 AND score = 70.00"); | ||
| 386 | + | ||
| 387 | + // --- 子查询 --- | ||
| 388 | + System.out.println("[DQL-07] 子查询 - SELECT 中嵌套子查询"); | ||
| 389 | + try (ResultSet rs = stmt.executeQuery( | ||
| 390 | + "SELECT name, age FROM " + TEST_TABLE | ||
| 391 | + + " WHERE age > (SELECT AVG(age) FROM " + TEST_TABLE + ") ORDER BY age")) { | ||
| 392 | + List<String> aboveAvg = new ArrayList<>(); | ||
| 393 | + while (rs.next()) { | ||
| 394 | + aboveAvg.add(rs.getString("name") + "(" + rs.getInt("age") + ")"); | ||
| 395 | + } | ||
| 396 | + System.out.println(" PASS | 年龄高于平均值的记录:" + String.join(", ", aboveAvg)); | ||
| 397 | + } | ||
| 398 | + } | ||
| 399 | + | ||
| 400 | + System.out.println("=> DQL 全部 7 项查询验证通过(基础SELECT/WHERE/ORDER+LIMIT/聚合/GROUP+HAVING/DISTINCT/子查询)。"); | ||
| 401 | + } | ||
| 402 | + | ||
| 403 | + // ==================== DCL ==================== | ||
| 404 | + private static void verifyDCL(HikariDataSource dataSource) throws Exception { | ||
| 405 | + printSection("DCL(Data Control Language)验证"); | ||
| 406 | + printPurpose("验证 GRANT / REVOKE 权限管理操作通过 dolphin MySQL 协议正常执行。"); | ||
| 407 | + System.out.println(" 注意:DCL 操作需要当前用户具备相应权限(如超级用户或对象属主)。"); | ||
| 408 | + | ||
| 409 | + try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement()) { | ||
| 410 | + | ||
| 411 | + // --- GRANT SELECT --- | ||
| 412 | + System.out.println("[DCL-01] GRANT - 授予表级 SELECT 权限"); | ||
| 413 | + try { | ||
| 414 | + stmt.executeUpdate("GRANT SELECT ON TABLE " + TEST_TABLE + " TO " + USERNAME); | ||
| 415 | + dclPassCount++; | ||
| 416 | + System.out.println(" PASS | GRANT SELECT ON TABLE " + TEST_TABLE + " TO " + USERNAME + " 执行成功"); | ||
| 417 | + } catch (SQLException e) { | ||
| 418 | + dclWarnCount++; | ||
| 419 | + System.out.println(" WARN | GRANT SELECT 失败(可能权限不足或已拥有):" + firstLineOrEmpty(e.getMessage())); | ||
| 420 | + } | ||
| 421 | + | ||
| 422 | + // --- GRANT INSERT/UPDATE/DELETE --- | ||
| 423 | + System.out.println("[DCL-02] GRANT - 授予多权限"); | ||
| 424 | + try { | ||
| 425 | + stmt.executeUpdate("GRANT INSERT, UPDATE, DELETE ON TABLE " + TEST_TABLE + " TO " + USERNAME); | ||
| 426 | + dclPassCount++; | ||
| 427 | + System.out.println(" PASS | GRANT INSERT,UPDATE,DELETE ON TABLE " + TEST_TABLE + " TO " + USERNAME + " 执行成功"); | ||
| 428 | + } catch (SQLException e) { | ||
| 429 | + dclWarnCount++; | ||
| 430 | + System.out.println(" WARN | GRANT 多权限失败(可能权限不足或已拥有):" + firstLineOrEmpty(e.getMessage())); | ||
| 431 | + } | ||
| 432 | + | ||
| 433 | + // --- REVOKE --- | ||
| 434 | + System.out.println("[DCL-03] REVOKE - 收回权限"); | ||
| 435 | + try { | ||
| 436 | + stmt.executeUpdate("REVOKE INSERT, UPDATE, DELETE ON TABLE " + TEST_TABLE + " FROM " + USERNAME); | ||
| 437 | + dclPassCount++; | ||
| 438 | + System.out.println(" PASS | REVOKE INSERT,UPDATE,DELETE ON TABLE " + TEST_TABLE + " FROM " + USERNAME + " 执行成功"); | ||
| 439 | + } catch (SQLException e) { | ||
| 440 | + dclWarnCount++; | ||
| 441 | + System.out.println(" WARN | REVOKE 失败(可能权限不足):" + firstLineOrEmpty(e.getMessage())); | ||
| 442 | + } | ||
| 443 | + | ||
| 444 | + // --- 查询当前用户权限 --- | ||
| 445 | + System.out.println("[DCL-04] 查询当前用户权限信息"); | ||
| 446 | + try (ResultSet rs = stmt.executeQuery( | ||
| 447 | + "SELECT privilege_type, is_grantable FROM information_schema.table_privileges " | ||
| 448 | + + "WHERE table_name = '" + TEST_TABLE.toLowerCase() + "' AND grantee = CURRENT_USER LIMIT 10")) { | ||
| 449 | + List<String> privs = new ArrayList<>(); | ||
| 450 | + while (rs.next()) { | ||
| 451 | + privs.add(rs.getString("privilege_type") | ||
| 452 | + + ("YES".equalsIgnoreCase(rs.getString("is_grantable")) ? "(可转授)" : "")); | ||
| 453 | + } | ||
| 454 | + if (privs.isEmpty()) { | ||
| 455 | + dclInfoCount++; | ||
| 456 | + System.out.println(" INFO | 当前用户对 " + TEST_TABLE + " 无额外授权记录(使用默认角色权限)"); | ||
| 457 | + } else { | ||
| 458 | + dclPassCount++; | ||
| 459 | + System.out.println(" PASS | 当前用户权限:" + String.join(", ", privs)); | ||
| 460 | + } | ||
| 461 | + } | ||
| 462 | + } | ||
| 463 | + | ||
| 464 | + StringBuilder dclSummary = new StringBuilder("=> DCL 权限管理操作验证完成(GRANT/REVOKE/权限查询):PASS " | ||
| 465 | + + dclPassCount + " 项"); | ||
| 466 | + if (dclWarnCount > 0) { | ||
| 467 | + dclSummary.append(",WARN ").append(dclWarnCount).append(" 项(权限不足)"); | ||
| 468 | + } | ||
| 469 | + if (dclInfoCount > 0) { | ||
| 470 | + dclSummary.append(",INFO ").append(dclInfoCount).append(" 项"); | ||
| 471 | + } | ||
| 472 | + System.out.println(dclSummary.append("。").toString()); | ||
| 473 | + | ||
| 474 | + // 恢复 DCL 测试中可能收回的权限,确保后续 TCL / 并发测试可正常执行 | ||
| 475 | + try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement()) { | ||
| 476 | + try { stmt.executeUpdate("GRANT ALL ON TABLE " + TEST_TABLE + " TO " + USERNAME); } catch (Exception ignored) {} | ||
| 477 | + } | ||
| 478 | + } | ||
| 479 | + | ||
| 480 | + // ==================== TCL ==================== | ||
| 481 | + private static void verifyTCL(HikariDataSource dataSource) throws Exception { | ||
| 482 | + printSection("TCL(Transaction Control Language)验证"); | ||
| 483 | + printPurpose("验证 COMMIT / ROLLBACK / SAVEPOINT 事务控制操作通过 dolphin MySQL 协议正常执行。"); | ||
| 484 | + | ||
| 485 | + try (Connection conn = dataSource.getConnection()) { | ||
| 486 | + | ||
| 487 | + // --- 显式 COMMIT --- | ||
| 488 | + System.out.println("[TCL-01] COMMIT - 显式提交事务"); | ||
| 489 | + conn.setAutoCommit(false); | ||
| 490 | + try (Statement stmt = conn.createStatement()) { | ||
| 491 | + stmt.executeUpdate("DELETE FROM " + TEST_TABLE + " WHERE name = 'tcl_commit_test'"); | ||
| 492 | + stmt.executeUpdate("INSERT INTO " + TEST_TABLE + " (name, age, score) VALUES ('tcl_commit_test', 40, 77.00)"); | ||
| 493 | + } | ||
| 494 | + conn.commit(); | ||
| 495 | + int committedCount = queryCountByName(conn, TEST_TABLE, "tcl_commit_test"); | ||
| 496 | + if (committedCount == 1) { | ||
| 497 | + System.out.println(" PASS | COMMIT 后 tcl_commit_test 数据可见,行数=" + committedCount); | ||
| 498 | + } else { | ||
| 499 | + throw new IllegalStateException("COMMIT 失败:预期 1 行,实际 " + committedCount); | ||
| 500 | + } | ||
| 501 | + | ||
| 502 | + // --- ROLLBACK --- | ||
| 503 | + System.out.println("[TCL-02] ROLLBACK - 回滚事务"); | ||
| 504 | + conn.setAutoCommit(false); | ||
| 505 | + try (Statement stmt = conn.createStatement()) { | ||
| 506 | + stmt.executeUpdate("INSERT INTO " + TEST_TABLE + " (name, age, score) VALUES ('tcl_rollback_test', 41, 66.00)"); | ||
| 507 | + } | ||
| 508 | + // 不 commit,直接 rollback | ||
| 509 | + conn.rollback(); | ||
| 510 | + int rolledBackCount = queryCountByName(conn, TEST_TABLE, "tcl_rollback_test"); | ||
| 511 | + if (rolledBackCount == 0) { | ||
| 512 | + System.out.println(" PASS | ROLLBACK 后 tcl_rollback_test 数据不存在,行数=" + rolledBackCount); | ||
| 513 | + } else { | ||
| 514 | + throw new IllegalStateException("ROLLBACK 失败:预期 0 行,实际 " + rolledBackCount); | ||
| 515 | + } | ||
| 516 | + | ||
| 517 | + // --- SAVEPOINT + ROLLBACK TO SAVEPOINT --- | ||
| 518 | + System.out.println("[TCL-03] SAVEPOINT + ROLLBACK TO SAVEPOINT - 部分回滚"); | ||
| 519 | + conn.setAutoCommit(false); | ||
| 520 | + try (Statement stmt = conn.createStatement()) { | ||
| 521 | + stmt.executeUpdate("DELETE FROM " + TEST_TABLE + " WHERE name IN ('sp_keep', 'sp_discard')"); | ||
| 522 | + stmt.executeUpdate("INSERT INTO " + TEST_TABLE + " (name, age, score) VALUES ('sp_keep', 42, 55.00)"); | ||
| 523 | + stmt.executeUpdate("SAVEPOINT sp1"); | ||
| 524 | + stmt.executeUpdate("INSERT INTO " + TEST_TABLE + " (name, age, score) VALUES ('sp_discard', 43, 44.00)"); | ||
| 525 | + stmt.executeUpdate("ROLLBACK TO SAVEPOINT sp1"); | ||
| 526 | + } | ||
| 527 | + int keepCount = queryCountByName(conn, TEST_TABLE, "sp_keep"); | ||
| 528 | + int discardCount = queryCountByName(conn, TEST_TABLE, "sp_discard"); | ||
| 529 | + conn.commit(); // 提交保留的部分 | ||
| 530 | + System.out.println(" PASS | ROLLBACK TO SAVEPOINT 后:sp_keep 行数=" + keepCount + "(保留),sp_discard 行数=" + discardCount + "(回滚)"); | ||
| 531 | + | ||
| 532 | + // --- 隔离级别查询 --- | ||
| 533 | + System.out.println("[TCL-04] 事务隔离级别确认"); | ||
| 534 | + String isolation = ""; | ||
| 535 | + try (ResultSet rs = conn.createStatement().executeQuery("SELECT @@session.transaction_isolation AS iso")) { | ||
| 536 | + if (rs.next()) isolation = rs.getString("iso"); | ||
| 537 | + } | ||
| 538 | + System.out.println(" INFO | 当前事务隔离级别:" + isolation); | ||
| 539 | + | ||
| 540 | + // 恢复 autoCommit | ||
| 541 | + conn.setAutoCommit(true); | ||
| 542 | + } | ||
| 543 | + | ||
| 544 | + System.out.println("=> TCL 共 3 项事务控制验证通过(COMMIT/ROLLBACK/SAVEPOINT+RB_TO_SP);隔离级别查询为 INFO(仅查询,不计入 PASS)。"); | ||
| 545 | + } | ||
| 546 | + | ||
| 547 | + // ==================== 并发连接 ==================== | ||
| 548 | + private static void verifyConcurrentConnections(HikariDataSource dataSource) throws Exception { | ||
| 549 | + printSection("并发连接验证"); | ||
| 550 | + printPurpose("启动多个线程同时从 HikariCP 连接池获取连接并执行 SQL,验证并发场景下连接不泄漏、SQL 正确执行。"); | ||
| 551 | + | ||
| 552 | + int threadCount = 10; | ||
| 553 | + ExecutorService executor = Executors.newFixedThreadPool(threadCount); | ||
| 554 | + CountDownLatch ready = new CountDownLatch(threadCount); | ||
| 555 | + CountDownLatch startGate = new CountDownLatch(1); | ||
| 556 | + AtomicInteger passed = new AtomicInteger(0); | ||
| 557 | + AtomicInteger failed = new AtomicInteger(0); | ||
| 558 | + | ||
| 559 | + System.out.println("启动 " + threadCount + " 个并发线程..."); | ||
| 560 | + | ||
| 561 | + for (int i = 0; i < threadCount; i++) { | ||
| 562 | + final int idx = i; | ||
| 563 | + executor.submit(() -> { | ||
| 564 | + String workerName = "conc_worker_" + idx + "_" + System.nanoTime(); | ||
| 565 | + ready.countDown(); | ||
| 566 | + try { | ||
| 567 | + startGate.await(); | ||
| 568 | + try (Connection conn = dataSource.getConnection()) { | ||
| 569 | + conn.setAutoCommit(false); | ||
| 570 | + // INSERT | ||
| 571 | + try (PreparedStatement ps = conn.prepareStatement( | ||
| 572 | + "INSERT INTO " + TEST_TABLE + " (name, age, score) VALUES (?, ?, ?)")) { | ||
| 573 | + ps.setString(1, workerName); | ||
| 574 | + ps.setInt(2, 20 + idx); | ||
| 575 | + ps.setDouble(3, 60.0 + idx); | ||
| 576 | + ps.executeUpdate(); | ||
| 577 | + } | ||
| 578 | + // SELECT 验证 | ||
| 579 | + int cnt = queryCountByName(conn, TEST_TABLE, workerName); | ||
| 580 | + if (cnt != 1) throw new IllegalStateException("并发 INSERT 后查不到数据"); | ||
| 581 | + // UPDATE | ||
| 582 | + try (PreparedStatement ps = conn.prepareStatement( | ||
| 583 | + "UPDATE " + TEST_TABLE + " SET age = age + 1 WHERE name = ?")) { | ||
| 584 | + ps.setString(1, workerName); | ||
| 585 | + ps.executeUpdate(); | ||
| 586 | + } | ||
| 587 | + // DELETE | ||
| 588 | + try (PreparedStatement ps = conn.prepareStatement( | ||
| 589 | + "DELETE FROM " + TEST_TABLE + " WHERE name = ?")) { | ||
| 590 | + ps.setString(1, workerName); | ||
| 591 | + ps.executeUpdate(); | ||
| 592 | + } | ||
| 593 | + conn.commit(); | ||
| 594 | + passed.incrementAndGet(); | ||
| 595 | + } | ||
| 596 | + } catch (Exception e) { | ||
| 597 | + failed.incrementAndGet(); | ||
| 598 | + System.err.println(" Worker-" + idx + " 失败: " + e.getMessage()); | ||
| 599 | + } | ||
| 600 | + }); | ||
| 601 | + } | ||
| 602 | + | ||
| 603 | + ready.await(10, TimeUnit.SECONDS); | ||
| 604 | + startGate.countDown(); | ||
| 605 | + executor.shutdown(); | ||
| 606 | + boolean terminated = executor.awaitTermination(30, TimeUnit.SECONDS); | ||
| 607 | + | ||
| 608 | + System.out.println(" 并发线程总数:" + threadCount); | ||
| 609 | + System.out.println(" 通过:" + passed.get() + " / 失败:" + failed.get()); | ||
| 610 | + System.out.println(" 线程池终止:" + (terminated ? "正常" : "超时")); | ||
| 611 | + | ||
| 612 | + if (passed.get() != threadCount || failed.get() != 0) { | ||
| 613 | + throw new IllegalStateException("并发验证失败:通过 " + passed.get() + "/" + threadCount); | ||
| 614 | + } | ||
| 615 | + System.out.println(" PASS | " + threadCount + "/" + threadCount + " 个并发 worker 均完成 insert/select/update/delete/commit。"); | ||
| 616 | + System.out.println("=> 并发连接验证通过。"); | ||
| 617 | + } | ||
| 618 | + | ||
| 619 | + // ==================== 清理 ==================== | ||
| 620 | + private static void cleanupTables(HikariDataSource dataSource) throws Exception { | ||
| 621 | + printSection("清理 DDL 残留表"); | ||
| 622 | + try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement()) { | ||
| 623 | + try { stmt.executeUpdate("DROP TABLE IF EXISTS " + TEST_TABLE); System.out.println(" 已清理 " + TEST_TABLE); } catch (Exception ignored) {} | ||
| 624 | + try { stmt.executeUpdate("DROP TABLE IF EXISTS " + TEST_TABLE_2); System.out.println(" 已清理 " + TEST_TABLE_2); } catch (Exception ignored) {} | ||
| 625 | + } | ||
| 626 | + } | ||
| 627 | + | ||
| 628 | + // ==================== 结论 ==================== | ||
| 629 | + private static void printFinalConclusion() { | ||
| 630 | + printSection("最终结论"); | ||
| 631 | + String border = "+" + "-".repeat(CATEGORY_WIDTH) + "+" + "-".repeat(OPERATION_WIDTH) + "+" | ||
| 632 | + + "-".repeat(RESULT_WIDTH) + "+"; | ||
| 633 | + | ||
| 634 | + String dmlResult = generatedKeysReturned ? "PASS (5项)" : "PASS (4项)"; | ||
| 635 | + String dclResult = dclWarnCount == 0 | ||
| 636 | + ? "PASS (" + dclPassCount + "项)" | ||
| 637 | + : "PASS (" + dclPassCount + "项) + WARN (" + dclWarnCount + "项)"; | ||
| 638 | + | ||
| 639 | + System.out.println(border); | ||
| 640 | + System.out.println(row("分类", "操作类型", "验证结果")); | ||
| 641 | + System.out.println(border); | ||
| 642 | + System.out.println(row("DDL", "CREATE / ALTER / DROP", "PASS (7项)")); | ||
| 643 | + System.out.println(row("", "TRUNCATE / CTAS", "")); | ||
| 644 | + System.out.println(border); | ||
| 645 | + System.out.println(row("DML", "INSERT / UPDATE / DELETE", dmlResult)); | ||
| 646 | + System.out.println(row("", "批量写入", "")); | ||
| 647 | + if (!generatedKeysReturned) { | ||
| 648 | + System.out.println(row("", "getGeneratedKeys", "INFO (1项)")); | ||
| 649 | + } | ||
| 650 | + System.out.println(border); | ||
| 651 | + System.out.println(row("DQL", "SELECT / WHERE / ORDER BY", "PASS (7项)")); | ||
| 652 | + System.out.println(row("", "聚合 / GROUP BY / 子查询", "")); | ||
| 653 | + System.out.println(border); | ||
| 654 | + System.out.println(row("DCL", "GRANT / REVOKE / 权限查询", dclResult)); | ||
| 655 | + if (dclInfoCount > 0) { | ||
| 656 | + System.out.println(row("", "无额外授权记录", "INFO (" + dclInfoCount + "项)")); | ||
| 657 | + } | ||
| 658 | + System.out.println(border); | ||
| 659 | + System.out.println(row("TCL", "COMMIT / ROLLBACK", "PASS (3项)")); | ||
| 660 | + System.out.println(row("", "SAVEPOINT+RB_TO_SP", "")); | ||
| 661 | + System.out.println(row("", "隔离级别查询", "INFO (1项)")); | ||
| 662 | + System.out.println(border); | ||
| 663 | + System.out.println(row("并发", "10 worker 同时持连接执行SQL", "PASS")); | ||
| 664 | + System.out.println(border); | ||
| 665 | + System.out.println(); | ||
| 666 | + | ||
| 667 | + StringBuilder conclusion = new StringBuilder("DDL/DML/DQL/DCL/TCL + 并发连接验证通过;"); | ||
| 668 | + if (!generatedKeysReturned) { | ||
| 669 | + conclusion.append("DML-05 getGeneratedKeys 与 "); | ||
| 670 | + } | ||
| 671 | + conclusion.append("TCL-04 隔离级别查询为 INFO,不计入 PASS。"); | ||
| 672 | + System.out.println(conclusion.toString()); | ||
| 673 | + System.out.println("HikariCP + MySQL Connector/J 经 dolphin MySQL 协议访问 openGauss B 兼容库功能完整可用。"); | ||
🔵 Low Priority 本验证工程的核心价值是给出"28 项全部 PASS"的兼容性证据,但代码中绝大多数验证项只打印结果、不做断言:DML-01/02/03/04(如第 207 行 UPDATE 影响行数仅打印不校验,若 Alice 行不存在会打印 PASS | UPDATE 影响行数=0)、DQL-01 至 DQL-07 全部只打印行数与数据、DCL 全部走 WARN/PASS 分支,仅 DDL-05 TRUNCATE 计数、TCL-01/02 计数与并发通过数有硬性断言(4/28 项)。同时 printFinalConclusion()(第 587-610 行)无条件输出"全部五类 SQL 操作 + 并发连接验证通过",即使中间某项 SQL 静默失败、返回错误数据,最终结论仍显示 PASS。触发条件:任一条 SQL 执行成功但结果错误(如 UPDATE 影响 0 行、DISTINCT 去重数量不对、聚合结果错误),程序仍输出 PASS 并正常结束,导致"实测证据"失真。修复方向:为每项验证补充与预期值的断言(如影响行数、返回行数、关键字段值),任一断言失败即抛异常终止并输出 FAILED。 建议:为每个验证项增加对预期结果(影响行数/返回行数/关键值)的断言,失败即抛出 IllegalStateException;printFinalConclusion 仅在全部断言通过后调用,或改为根据已统计的失败数输出 PASS/FAILED 两种结论。 ![]() ![]() | |||
| 674 | + } | ||
| 675 | + | ||
| 676 | + private static String row(String category, String operation, String result) { | ||
| 677 | + return "|" + cell(category, CATEGORY_WIDTH) + "|" + cell(operation, OPERATION_WIDTH) + "|" | ||
| 678 | + + cell(result, RESULT_WIDTH) + "|"; | ||
| 679 | + } | ||
| 680 | + | ||
| 681 | + /** 生成固定宽度的表格单元格,中文按两列宽度补齐空格。 */ | ||
| 682 | + private static String cell(String text, int width) { | ||
| 683 | + StringBuilder content = new StringBuilder(" ").append(text); | ||
| 684 | + int padding = width - 1 - displayWidth(text); | ||
| 685 | + for (int i = 0; i < padding; i++) { | ||
| 686 | + content.append(' '); | ||
| 687 | + } | ||
| 688 | + return content.toString(); | ||
| 689 | + } | ||
| 690 | + | ||
| 691 | + /** 按中文字符占两列的方式计算显示宽度。 */ | ||
| 692 | + private static int displayWidth(String text) { | ||
| 693 | + int width = 0; | ||
| 694 | + for (int i = 0; i < text.length(); i++) { | ||
| 695 | + char c = text.charAt(i); | ||
| 696 | + width += (c >= '\u2E80' && c <= '\u9FFF') ? 2 : 1; | ||
| 697 | + } | ||
| 698 | + return width; | ||
| 699 | + } | ||
| 700 | + | ||
| 701 | + // ==================== 工具方法 ==================== | ||
| 702 | + private static void printSection(String title) { | ||
| 703 | + System.out.println(); | ||
| 704 | + System.out.println("==================== " + (sectionNo++) + ". " + title + " ===================="); | ||
| 705 | + } | ||
| 706 | + | ||
| 707 | + private static void printPurpose(String purpose) { | ||
| 708 | + System.out.println("测试目的:" + purpose); | ||
| 709 | + } | ||
| 710 | + | ||
| 711 | + private static int queryCount(Connection conn, String table) throws SQLException { | ||
| 712 | + try (Statement s = conn.createStatement(); | ||
| 713 | + ResultSet rs = s.executeQuery("SELECT COUNT(*) AS c FROM " + table)) { | ||
| 714 | + return rs.next() ? rs.getInt("c") : 0; | ||
| 715 | + } | ||
| 716 | + } | ||
| 717 | + | ||
| 718 | + private static int queryCountByName(Connection conn, String table, String name) throws SQLException { | ||
| 719 | + try (PreparedStatement ps = conn.prepareStatement("SELECT COUNT(*) AS c FROM " + table + " WHERE name = ?")) { | ||
| 720 | + ps.setString(1, name); | ||
| 721 | + try (ResultSet rs = ps.executeQuery()) { | ||
| 722 | + return rs.next() ? rs.getInt("c") : 0; | ||
| 723 | + } | ||
| 724 | + } | ||
| 725 | + } | ||
| 726 | + | ||
| 727 | + /** 读取必需的环境变量;缺失或为空时显式抛出,避免把明文口令写死在代码中。 */ | ||
| 728 | + private static String requireEnv(String key) { | ||
| 729 | + String v = System.getenv(key); | ||
| 730 | + if (v == null || v.isBlank()) { | ||
| 731 | + throw new IllegalStateException("缺少必需环境变量 " + key | ||
| 732 | + + ",请通过环境变量注入数据库连接口令后再运行本示例。"); | ||
| 733 | + } | ||
| 734 | + return v; | ||
| 735 | + } | ||
| 736 | + /** 安全获取异常消息的第一行;若消息为 null 或空,返回占位符。 */ | ||
| 737 | + private static String firstLineOrEmpty(String msg) { | ||
| 738 | + if (msg == null || msg.isEmpty()) { | ||
| 739 | + return "(无详细错误信息)"; | ||
| 740 | + } | ||
| 741 | + return msg.split("\n")[0]; | ||
| 742 | +} | ||
| 743 | +} | ||
| @@ -0,0 +1,49 @@ | |||
| 1 | +package com.linyu.ospp; | ||
| 2 | + | ||
| 3 | +public class User { | ||
| 4 | + private Integer id; | ||
| 5 | + private String name; | ||
| 6 | + private Integer age; | ||
| 7 | + | ||
| 8 | + public User() { | ||
| 9 | + } | ||
| 10 | + | ||
| 11 | + public User(Integer id, String name, Integer age) { | ||
| 12 | + this.id = id; | ||
| 13 | + this.name = name; | ||
| 14 | + this.age = age; | ||
| 15 | + } | ||
| 16 | + | ||
| 17 | + public Integer getId() { | ||
| 18 | + return id; | ||
| 19 | + } | ||
| 20 | + | ||
| 21 | + public void setId(Integer id) { | ||
| 22 | + this.id = id; | ||
| 23 | + } | ||
| 24 | + | ||
| 25 | + public String getName() { | ||
| 26 | + return name; | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + public void setName(String name) { | ||
| 30 | + this.name = name; | ||
| 31 | + } | ||
| 32 | + | ||
| 33 | + public Integer getAge() { | ||
| 34 | + return age; | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + public void setAge(Integer age) { | ||
| 38 | + this.age = age; | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + | ||
| 42 | + public String toString() { | ||
| 43 | + return "User{" + | ||
| 44 | + "id=" + id + | ||
| 45 | + ", name='" + name + '\'' + | ||
| 46 | + ", age=" + age + | ||
| 47 | + '}'; | ||
| 48 | + } | ||
| 49 | +} | ||
| @@ -0,0 +1,136 @@ | |||
| 1 | +package org.hikaritest; | ||
| 2 | + | ||
| 3 | +import com.zaxxer.hikari.HikariConfig; | ||
| 4 | +import com.zaxxer.hikari.HikariDataSource; | ||
| 5 | + | ||
| 6 | +import java.sql.Connection; | ||
| 7 | +import java.sql.ResultSet; | ||
| 8 | +import java.sql.Statement; | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * HikariCP + MySQL JDBC Driver 连接 openGauss B 兼容库(dolphin 插件)的连通性验证示例。 | ||
| 12 | + * | ||
| 13 | + * <p>背景:开源之夏 2024 的兼容性测试报告(doc/测试报告.md)指出,在 openGauss 6.0.0-RC1 上, | ||
| 14 | + * HikariCP 通过 MySQL 驱动无法创建有效连接,原因是 dolphin 对 { SELECT @.transaction_isolation} | ||
| 15 | + * 返回无法映射的隔离级别 'default'。本示例在 openGauss 7.0.0-RC3 上验证:使用正确的连接参数后, | ||
| 16 | + * HikariCP + MySQL Connector/J 可以正常建立连接并完成 CRUD。</p> | ||
| 17 | + * | ||
| 18 | + * <p>运行前请修改下方连接常量,确保: | ||
| 19 | + * <ol> | ||
| 20 | + * <li>目标库为 B 兼容库,且已 { CREATE EXTENSION dolphin;}</li> | ||
| 21 | + * <li>连接用户已通过 { SELECT set_native_password('user','password','%');} 设置 MySQL 原生密码</li> | ||
| 22 | + * <li>服务端已开启 MySQL 协议端口(默认 3306)</li> | ||
| 23 | + * </ol> | ||
| 24 | + * </p> | ||
| 25 | + */ | ||
| 26 | +public class HikariMySQLVerify { | ||
| 27 | + | ||
| 28 | + // ===== 连接参数:优先从环境变量读取,避免明文口令进入版本库 ===== | ||
| 29 | + private static final String JDBC_URL = | ||
| 30 | + System.getenv().getOrDefault("OPENGAUSS_MYSQL_URL", | ||
| 31 | + "jdbc:mysql://127.0.0.1:3306/mysql_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true&characterEncoding=utf8"); | ||
| 32 | + private static final String USER = System.getenv().getOrDefault("OPENGAUSS_MYSQL_USER", "mysqluser"); | ||
| 33 | + private static final String PASSWORD = requireEnv("OPENGAUSS_MYSQL_PASSWORD"); | ||
| 34 | + // =========================== | ||
| 35 | + | ||
| 36 | + private static String requireEnv(String key) { | ||
| 37 | + String v = System.getenv(key); | ||
| 38 | + if (v == null || v.isBlank()) { | ||
| 39 | + throw new IllegalStateException("缺少必需环境变量 " + key | ||
| 40 | + + ",请通过环境变量注入数据库连接口令后再运行本示例。"); | ||
| 41 | + } | ||
| 42 | + return v; | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + private static final String TEST_TABLE = "hikari_verify_demo"; | ||
| 46 | + | ||
| 47 | + public static void main(String[] args) { | ||
| 48 | + HikariConfig config = new HikariConfig(); | ||
| 49 | + config.setJdbcUrl(JDBC_URL); | ||
| 50 | + config.setUsername(USER); | ||
| 51 | + config.setPassword(PASSWORD); | ||
| 52 | + config.setDriverClassName("com.mysql.cj.jdbc.Driver"); | ||
| 53 | + // 连接池参数(与 openGauss 服务端超时设置保持协调) | ||
| 54 | + config.setMaximumPoolSize(10); | ||
| 55 | + config.setMinimumIdle(2); | ||
| 56 | + config.setConnectionTimeout(30_000); | ||
| 57 | + config.setIdleTimeout(600_000); | ||
| 58 | + config.setMaxLifetime(1_800_000); | ||
| 59 | + config.setConnectionTestQuery("SELECT 1"); | ||
| 60 | + config.setPoolName("HikariCP-openGauss-Verify"); | ||
| 61 | + | ||
| 62 | + int failed = 0; | ||
| 63 | + try (HikariDataSource ds = new HikariDataSource(config)) { | ||
| 64 | + System.out.println("HikariCP dataSource created OK, poolName=" + ds.getPoolName()); | ||
| 65 | + | ||
| 66 | + // 1) 基础连通 | ||
| 67 | + try (Connection conn = ds.getConnection(); | ||
| 68 | + Statement st = conn.createStatement()) { | ||
| 69 | + try (ResultSet rs = st.executeQuery("SELECT 1")) { | ||
| 70 | + if (rs.next() && rs.getInt(1) == 1) { | ||
| 71 | + System.out.println("SELECT 1 -> " + rs.getInt(1)); | ||
| 72 | + } else { | ||
| 73 | + System.out.println("SELECT 1 FAILED"); | ||
| 74 | + failed++; | ||
| 75 | + } | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + // 2) 版本与协议信息 | ||
| 79 | + try (ResultSet rs = st.executeQuery("SELECT version()")) { | ||
| 80 | + if (rs.next()) { | ||
| 81 | + System.out.println("version() -> " + rs.getString(1)); | ||
| 82 | + } | ||
| 83 | + } | ||
| 84 | + } | ||
| 85 | + | ||
| 86 | + // 3) CRUD 流程 | ||
| 87 | + try (Connection conn = ds.getConnection(); | ||
| 88 | + Statement st = conn.createStatement()) { | ||
| 89 | + st.execute("DROP TABLE IF EXISTS " + TEST_TABLE); | ||
| 90 | + st.execute("CREATE TABLE " + TEST_TABLE + " (id INT PRIMARY KEY, name VARCHAR(64))"); | ||
| 91 | + st.execute("INSERT INTO " + TEST_TABLE + " VALUES (1,'hikari'),(2,'opengauss')"); | ||
| 92 | + | ||
| 93 | + try (ResultSet rs = st.executeQuery("SELECT id, name FROM " + TEST_TABLE + " ORDER BY id")) { | ||
| 94 | + int rows = 0; | ||
| 95 | + while (rs.next()) { | ||
| 96 | + System.out.println("row -> " + rs.getInt("id") + ", " + rs.getString("name")); | ||
| 97 | + rows++; | ||
| 98 | + } | ||
| 99 | + if (rows != 2) { | ||
| 100 | + System.out.println("CRUD row count unexpected: " + rows); | ||
| 101 | + failed++; | ||
| 102 | + } | ||
| 103 | + } | ||
| 104 | + | ||
| 105 | + // 4) 事务验证 | ||
| 106 | + conn.setAutoCommit(false); | ||
| 107 | + st.execute("UPDATE " + TEST_TABLE + " SET name='hikari-cp' WHERE id=1"); | ||
| 108 | + conn.commit(); | ||
| 109 | + try (ResultSet rs = st.executeQuery("SELECT name FROM " + TEST_TABLE + " WHERE id=1")) { | ||
| 110 | + if (rs.next() && "hikari-cp".equals(rs.getString(1))) { | ||
| 111 | + System.out.println("transaction commit OK -> " + rs.getString(1)); | ||
| 112 | + } else { | ||
| 113 | + System.out.println("transaction verify FAILED"); | ||
| 114 | + failed++; | ||
| 115 | + } | ||
| 116 | + } | ||
| 117 | + conn.setAutoCommit(true); | ||
| 118 | + | ||
| 119 | + st.execute("DROP TABLE IF EXISTS " + TEST_TABLE); | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + if (failed == 0) { | ||
| 123 | + System.out.println("== HikariCP + MySQL JDBC verify PASSED =="); | ||
| 124 | + System.exit(0); | ||
| 125 | + } else { | ||
| 126 | + System.out.println("== HikariCP + MySQL JDBC verify FAILED (" + failed + ") =="); | ||
| 127 | + System.exit(1); | ||
| 128 | + } | ||
| 129 | + } catch (Exception e) { | ||
| 130 | + // 覆盖 SQLException 与 Hikari 建池阶段可能抛出的 RuntimeException(如无法建立连接) | ||
| 131 | + System.err.println("HikariCP 连接或初始化失败: " + e.getMessage()); | ||
| 132 | + e.printStackTrace(); | ||
| 133 | + System.exit(2); | ||
| 134 | + } | ||
| 135 | + } | ||
| 136 | +} | ||
| @@ -0,0 +1,22 @@ | |||
| 1 | +spring: | ||
| 2 | + main: | ||
| 3 | + web-application-type: none | ||
| 4 | + application: | ||
| 5 | + name: spring-boot-ospp | ||
| 6 | + datasource: | ||
| 7 | + url: ${OPENGAUSS_MYSQL_URL:jdbc:mysql://127.0.0.1:3306/mysql_db?useSSL=false&serverTimezone=UTC&characterEncoding=utf-8&allowPublicKeyRetrieval=true} | ||
| 8 | + username: ${OPENGAUSS_MYSQL_USER:mysqluser} | ||
| 9 | + password: ${OPENGAUSS_MYSQL_PASSWORD:xxxxxx} | ||
| 10 | + driver-class-name: com.mysql.cj.jdbc.Driver | ||
| 11 | + hikari: | ||
| 12 | + pool-name: HikariCP-openGauss-OSPP | ||
| 13 | + maximum-pool-size: 10 | ||
| 14 | + minimum-idle: 2 | ||
| 15 | + connection-timeout: 30000 | ||
| 16 | + idle-timeout: 300000 | ||
| 17 | + max-lifetime: 540000 | ||
| 18 | + keepalive-time: 300000 | ||
| 19 | + | ||
| 20 | +ospp: | ||
| 21 | + verify: | ||
| 22 | + enabled: ${OSPP_VERIFY_ENABLED:true} | ||
| @@ -0,0 +1,21 @@ | |||
| 1 | +package com.linyu.ospp; | ||
| 2 | + | ||
| 3 | +import org.junit.jupiter.api.Test; | ||
| 4 | +import org.springframework.boot.test.context.SpringBootTest; | ||
| 5 | +import org.springframework.test.context.TestPropertySource; | ||
| 6 | + | ||
| 7 | + | ||
| 8 | + | ||
| 9 | + "spring.datasource.hikari.initialization-fail-timeout=-1", | ||
| 10 | + "ospp.verify.enabled=false" | ||
| 11 | +}) | ||
| 12 | +class SpringBootOsppApplicationTests { | ||
| 13 | + | ||
| 14 | + | ||
| 15 | + void contextLoads() { | ||
| 16 | + // 仅验证 Spring 上下文可以正常加载,不依赖外部数据库。 | ||
| 17 | + // HikariCP 设置 initialization-fail-timeout=-1 后,连接池不会在启动时 fail-fast。 | ||
| 18 | + // 同时关闭 ospp.verify.enabled,避免启动验证 Runner 在无数据库环境下执行 SQL 校验。 | ||
| 19 | + } | ||
| 20 | + | ||
| 21 | +} | ||


🔵 Low Priority
变更行 35(spring端-通过清单.txt)记录服务器版本为
(openGauss 7.0.0 build 93bfec54) compiled at 2026-08-26 10:21:31 commit 1000 last mr 9389 ... x86_64-pc-linux-gnu,而本 PR 的另一份证据 openGauss端-验证清单.txt 第 6 行记录的是openGauss: 7.0.0-RC3 build f08516a2(官方 RC3 发布构建);同时本文件第 10 行显示本次运行发生在 Windows 主机cuicui_dada(C:\Users\cuicu...)、Java 17.0.19、时间 2026-08-27,而 openGauss端清单第 4-5 行记录的环境是 openEuler 20.03 (WSL)、主机名 liyang、生成时间 2026-08-15,PR 描述又声称依赖 JDK 11。→ 两份自验证据描述的并非同一套环境、同一服务器构建:Spring Boot 集成 13 项 PASS 的"实测证据"实际产生于本地编译的 dev 构建(commit 1000 / last mr 9389,非官方 7.0.0-RC3 发布版)和另一台主机。→ 失败模式:导师/用户按 PR 声称的"openGauss 7.0.0-RC3 + dolphin 5.2 + JDK 11"环境复现时,无法与提交的实测日志对应;而本 PR 的核心卖点恰是"7.0.0-RC3(dolphin 5.2)下已修复、可正常建连",该结论的证据却来自非发布构建,证据可信度与可复现性受损。→ 修复:统一三处描述——在 spring 端清单中标注真实构建(93bfec54)与真实主机/JDK,或重新在官方 7.0.0-RC3 发布构建 + 声明环境(JDK 11)下重跑并替换证据文件,使清单、README、PR 描述三者一致。建议:将 spring 端清单第 35 行的版本信息与实际运行的服务器构建对齐,并在清单头部补充真实运行环境(主机 cuicui_dada、Java 17.0.19、运行日期 2026-08-27);同时核对 openGauss端-验证清单.txt 第 6 行与 PR 描述,确认三者指向同一 openGauss 构建。若坚持声称官方 7.0.0-RC3,需在官方发布构建 + 声明环境下重新生成证据。