【YaShanDB】使用AI给YaShanDB开发R2DBC驱动
背景
崖山数据库作为一款发布没多少年的纯自研数据库,在内核设计上非常亮眼,而且也通过了国测,但是周边的应用生态还有不少欠缺。最近打算让HALO博客的后台支持崖山数据库,但是却发现崖山数据库官方竟然没有R2DBC驱动,就想着看能不能给崖山做一个。
AI CODING - CodeBuddy
前几天在CodeBuddy领了免费的1000 credits,正好用来试试看,毕竟也算稍微大一点的工程,我可不舍得烧copilot。
使用PLAN模式,模型Default,CodeBuddy会自动分析需求,获取所有必须的知识,然后起草计划,问我是否按这个计划开始执行
我打算开发yashandb的r2dbc驱动,但我手上只有yashandb的jdbc驱动,你排个计划,看怎么做。jdbc驱动jar文件我已经放到当前目录了


name: r2dbc-yashandb-driver
overview: 从零开发 YashanDB 的 R2DBC 驱动,复用已有 JDBC jar 包中的协议层,实现完整的 R2DBC SPI 接口,支持响应式非阻塞数据库访问。
todos:
- id: setup-project
content: 创建 Maven 项目骨架:配置 pom.xml,引入 r2dbc-spi、reactor-core、本地 JDBC jar 依赖,设置 Java 17 编译目标
status: completed- id: config-factory-provider
content: 实现配置与服务发现层:YashanDbConnectionConfiguration(Builder 模式)、YashanDbConnectionFactory、YashanDbConnectionFactoryProvider、YashanDbConnectionFactoryMetadata 及 SPI 注册文件
status: completed
dependencies:
- setup-project
- id: connection-statement
content: 使用 [subagent:code-explorer] 探查协议层 API,实现 YashanDbConnection(事务状态机、连接生命周期)和 YashanDbStatement(参数绑定、SQL 执行,boundedElastic 调度)
status: completed
dependencies:
- config-factory-provider
- id: result-row-metadata
content: 实现结果集层:YashanDbResult、YashanDbRow、YashanDbRowMetadata、YashanDbColumnMetadata,完成行数据按列名/索引读取与元数据暴露
status: completed
dependencies:
- connection-statement
- id: codecs
content: 实现 codec 子包:Codec 接口、DefaultCodecs 注册表及全部类型编解码器(String/Integer/Long/Double/BigDecimal/Date/Timestamp/Blob/Clob/ByteArray),集成 ExceptionFactory 异常映射
status: completed
dependencies:
- result-row-metadata
- id: tests
content: 编写单元测试:覆盖 ConnectionFactory URL 解析、事务状态机、参数绑定流、结果集映射及各编解码器正确性
status: completed
dependencies:
- codecs
用户需求
基于已有的
yashandb-jdbc-1.10.2.jar,开发 YashanDB 数据库的 R2DBC 响应式驱动,使 YashanDB 能够在 Spring WebFlux 等响应式框架中使用。产品概述
R2DBC(Reactive Relational Database Connectivity)驱动将 YashanDB 的数据库访问能力暴露给响应式编程生态。驱动复用 JDBC jar 中已有的
com.yashandb.protocol.*底层协议层和com.yashandb.conf.*配置解析层,在其上实现完整的 R2DBC SPI 接口,对外提供符合 R2DBC 规范的响应式 API。核心功能
- 连接管理:实现
ConnectionFactory和ConnectionSPI,支持r2dbc:yashandb://URL 格式,连接池友好- SQL 执行:实现
StatementSPI,支持带参数绑定的 DML/DDL/DQL 语句执行- 结果集处理:实现
Result和Row/RowMetadataSPI,支持 30+ 数据类型的 Java 类型映射- 事务控制:实现
beginTransaction/commitTransaction/rollbackTransaction,支持隔离级别设置- 非阻塞包装:通过 Reactor
Schedulers.boundedElastic()将同步 JDBC/Protocol 调用包装为响应式流- 连接配置:支持主机、端口、数据库名、用户名、密码、SSL 等连接属性
- 服务发现:通过
META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider注册驱动,支持ConnectionFactories.get()自动发现技术栈
| 层次 | 技术选型 |
| ---------- | --------------------------------------------------------------- |
| 语言 | Java 17 |
| 构建工具 | Maven 3.8+ |
| R2DBC SPI |io.r2dbc:r2dbc-spi:1.0.0.RELEASE|
| 响应式框架 | Project Reactor (reactor-core:3.6.x) |
| 本地依赖 |yashandb-jdbc-1.10.2.jar(system scope / local Maven repo) |
| 测试 | JUnit 5 +io.r2dbc:r2dbc-spi-test+ Testcontainers(可选) |
| 代码简化 | Lombok(可选) |
实现策略
策略选择:策略 A(协议层直接复用 + 阻塞调度包装)
JDBC jar 包含独立的
com.yashandb.protocol.*协议层,不依赖java.sql,可直接复用。整体思路:
- 复用协议层:直接使用
NativeProtocol、YasSocketConnection、Packet、ExecuteResult等底层类进行网络通信与结果读取,避免重复实现协议解析。- 复用配置层:使用
com.yashandb.conf.ConnectionUrl解析 R2DBC URL,YasProperty读取连接属性。- 复用类型层:使用
com.yashandb.protocol.accessor.*30+ 类型访问器 +com.yashandb.core.DataType枚举完成 SQL 类型到 Java 类型的双向映射。- 异步调度:由于 YashanDB 底层 Socket IO 是同步阻塞的,所有阻塞调用统一通过
Mono.fromCallable(...).subscribeOn(Schedulers.boundedElastic())包装,确保不阻塞 Reactor 事件循环线程。- 背压支持:
Result.map()返回Flux<T>,逐行发射,支持 Reactor 背压。关键技术决策
- 本地 jar 引入:通过 Maven
systemscope 或安装到本地.m2仓库(推荐后者,避免路径耦合),pom.xml配置<scope>compile</scope>。- R2DBC URL 格式:
r2dbc:yashandb://user:password@host:port/database,YashanDbConnectionFactoryProvider负责解析并委托给ConnectionUrl。- 连接状态机:
YashanDbConnection维护autoCommit、transactionStatus枚举状态,事务操作返回Mono<Void>。- 线程安全:Protocol 层连接对象不保证线程安全,通过
AtomicBoolean closed+Mono.defer串行化操作,避免并发问题。- 错误转换:将
com.yashandb.*异常统一转换为R2dbcException子类(R2dbcBadGrammarException、R2dbcNonTransientResourceException等)。
架构设计
graph TD A[应用层<br/>Spring Data R2DBC / R2DBC Client] -->|r2dbc-spi| B[ConnectionFactory SPI] B --> C[YashanDbConnectionFactory] C --> D[YashanDbConnection] D --> E[YashanDbStatement] E --> F[YashanDbResult] F --> G[YashanDbRow / YashanDbRowMetadata] subgraph r2dbc-yashandb 驱动内部 C D E F G H[YashanDbConnectionConfiguration] I[YashanDbConnectionFactoryProvider] J[YashanDbExceptionFactory] K[ColumnDecoder / TypeMapping] end subgraph yashandb-jdbc-1.10.2.jar 复用层 L[com.yashandb.protocol.*<br/>NativeProtocol / YasSocketConnection] M[com.yashandb.conf.*<br/>ConnectionUrl / YasProperty] N[com.yashandb.protocol.accessor.*<br/>类型访问器] O[com.yashandb.core.DataType] P[com.yashandb.parameter.*] end C --> H C --> L D --> L E --> L E --> P G --> N G --> O I --> M D --> J G --> K
目录结构
r2dbc-yashandb/ ├── pom.xml # [NEW] Maven 项目配置,引入 r2dbc-spi、reactor-core、本地 JDBC jar ├── yashandb-jdbc-1.10.2.jar # [EXISTING] 本地 JDBC 依赖 ├── src/ │ └── main/ │ ├── java/ │ │ └── io/r2dbc/yashandb/ │ │ ├── YashanDbConnectionFactory.java # [NEW] 实现 ConnectionFactory SPI;负责创建连接、持有 Configuration、实现 create() 返回 Mono<Connection> │ │ ├── YashanDbConnectionFactoryProvider.java # [NEW] 实现 ConnectionFactoryProvider SPI;解析 r2dbc:yashandb:// URL,构建 Configuration 并委托 Factory │ │ ├── YashanDbConnectionFactoryMetadata.java # [NEW] 实现 ConnectionFactoryMetadata,返回驱动名称 "YashanDB" │ │ ├── YashanDbConnectionConfiguration.java # [NEW] 不可变配置对象;持有 host/port/database/username/password/connectTimeout 等;提供 Builder 模式 │ │ ├── YashanDbConnection.java # [NEW] 实现 Connection SPI;封装 NativeProtocol 实例;实现 createStatement/beginTransaction/commitTransaction/rollbackTransaction/close;维护 autoCommit/事务状态 │ │ ├── YashanDbStatement.java # [NEW] 实现 Statement SPI;支持 bind()/bindNull();execute() 返回 Flux<Result>;内部通过 Mono.fromCallable 调度到 boundedElastic │ │ ├── YashanDbResult.java # [NEW] 实现 Result SPI;封装协议层 ExecuteResult;getRowsUpdated() 返回 Mono<Long>;map() 返回 Flux<T>,逐行发射 │ │ ├── YashanDbRow.java # [NEW] 实现 Row SPI;委托类型访问器完成列值解码;支持按列名和列索引取值 │ │ ├── YashanDbRowMetadata.java # [NEW] 实现 RowMetadata SPI;暴露列名、数据类型、可空性等元数据 │ │ ├── YashanDbColumnMetadata.java # [NEW] 实现 ColumnMetadata SPI;封装单列元数据(名称、R2DBC 类型、原生类型、精度/标度) │ │ ├── codec/ │ │ │ ├── Codec.java # [NEW] 编解码器接口;定义 canDecode/decode/canEncode/encode 方法签名 │ │ │ ├── DefaultCodecs.java # [NEW] 编解码器注册表;管理所有 Codec 实例;提供 decode(columnValue, targetType) 和 encode(value) 统一入口 │ │ │ ├── StringCodec.java # [NEW] String/Char/Varchar 类型编解码 │ │ │ ├── IntegerCodec.java # [NEW] INT/SMALLINT/TINYINT 编解码 │ │ │ ├── LongCodec.java # [NEW] BIGINT 编解码 │ │ │ ├── DoubleCodec.java # [NEW] FLOAT/DOUBLE 编解码 │ │ │ ├── BigDecimalCodec.java # [NEW] NUMBER/DECIMAL 编解码 │ │ │ ├── LocalDateCodec.java # [NEW] DATE 类型编解码 │ │ │ ├── LocalDateTimeCodec.java # [NEW] TIMESTAMP 类型编解码 │ │ │ ├── BlobCodec.java # [NEW] BLOB 类型编解码,返回 io.r2dbc.spi.Blob │ │ │ ├── ClobCodec.java # [NEW] CLOB 类型编解码,返回 io.r2dbc.spi.Clob │ │ │ └── ByteArrayCodec.java # [NEW] RAW/BINARY 字节数组编解码 │ │ └── util/ │ │ ├── Assert.java # [NEW] 参数断言工具,避免引入额外依赖 │ │ └── ExceptionFactory.java # [NEW] 统一异常转换工厂;将 yashandb 异常映射到 R2dbcException 子类 │ └── resources/ │ └── META-INF/services/ │ └── io.r2dbc.spi.ConnectionFactoryProvider # [NEW] SPI 注册文件,内容为 io.r2dbc.yashandb.YashanDbConnectionFactoryProvider │ └── src/ └── test/ └── java/ └── io/r2dbc/yashandb/ ├── YashanDbConnectionFactoryTest.java # [NEW] 单元测试:ConnectionFactory 构建、URL 解析 ├── YashanDbConnectionTest.java # [NEW] 单元测试:事务状态机、连接生命周期 ├── YashanDbStatementTest.java # [NEW] 单元测试:参数绑定、SQL 执行流 ├── YashanDbResultTest.java # [NEW] 单元测试:结果集映射、行读取 └── codec/ └── CodecTest.java # [NEW] 单元测试:各类型编解码正确性
关键代码结构
YashanDbConnectionConfigurationBuilderpublic final class YashanDbConnectionConfiguration { private final String host; private final int port; private final String database; private final String username; private final CharSequence password; private final Duration connectTimeout; // getters ... public static Builder builder() { return new Builder(); } public static final class Builder { public Builder host(String host) { ... } public Builder port(int port) { ... } public Builder database(String database) { ... } public Builder username(String username) { ... } public Builder password(CharSequence password) { ... } public Builder connectTimeout(Duration connectTimeout) { ... } public YashanDbConnectionConfiguration build() { ... } } }
Codec<T>接口public interface Codec<T> { boolean canDecode(int nativeType, Class<?> targetType); T decode(Object value, int nativeType, Class<? extends T> targetType); boolean canEncode(Object value); Parameter encode(Object value); }
YashanDbResult核心结构public final class YashanDbResult implements Result { // 覆盖 getRowsUpdated() -> Mono<Long> // 覆盖 map(BiFunction<Row, RowMetadata, T>) -> Flux<T> // 内部通过 Flux.fromIterable(rows).map(...) 发射行数据 }使用的 Agent 扩展
SubAgent
- code-explorer
- 用途:探索
yashandb-jdbc-1.10.2.jar内部 class 文件结构,精确定位NativeProtocol、YasSocketConnection、ExecuteResult、ConnectionUrl、YasProperty等关键类的方法签名,为驱动实现提供准确的 API 调用依据。- 预期产出:获得协议层关键类的构造方法、核心方法签名,确保驱动实现代码能正确调用 JDBC jar 中的 internal API,避免编译错误。错误。
后面他就啪啪啪自己在干了,我也去干其他活了,没管,烧了大概250 Credits后就开发完了,他自己编写的测试用例也通过了。
不过由于没有提前给他可用的崖山数据库服务端环境,因此他的测试没有真实连库,于是我给了个环境给他,然后他又自己去干了,测了不对他就自己改,直到他认为本次任务已完成


但是吧,r2dbc其实有一套标准测试,用于验证各个数据库提供的r2dbc驱动是否符合规范,于是我也让他去测测

测完后他发现他自己写的r2dbc驱动的确有问题,就自己修复了,都没找我确认。

很明显这里列名大小写他幻觉了。肯定有很多开发人员没搞懂数据库字段名大小写的原理,因此把AI给训错了。
列名大小写那里再确认一下执行的完整用例,YASHANDB 应该是按别名大小写返回的,但是要求别名上用双引号包裹,如果没有双引号,数据库服务端会转成大写,和ORACLE的表现一致,所以我不觉得应该在r2dbc驱动里强制转小写
然后他就去自己分析自己把这个问题修好了。
readme没有自动生成,让他生成一下,然后我发现了一个值得注意的地方
> 其中 2 个用例因 YashanDB 不支持的特性被 `@Disabled`:
> - `compoundStatement`:YashanDB 不支持在单条 `createStatement()` 中执行 `;` 分隔的多条语句。开启`allowMultiStmt=true`后也只能返回第一条语句的结果。
> - `returnGeneratedValues`:YashanDB JDBC 驱动对非 IDENTITY 列不支持 `RETURN_GENERATED_KEYS`
这就没办法了,崖山的细节还是有欠缺,当然ORACLE的R2DBC也不支持一次性执行多条语句哈,只是崖山通过开参数能支持允许,但只返回第一条语句的结果也是做得有些粗糙了。
至于 RETURN_GENERATED_KEYS,后来我让AI测了旧的JDBC驱动,竟然是可以跑通的,暂时不想管了,还是保持屏蔽吧。
另外有个对未来版本崖山有兼容性问题的一个点,目前崖山数据库的所有官方连接驱动里,对于数据库名称都是忽略的,一旦未来崖山支持了多个库(其实最新版内核已经支持多个库了,只是连接驱动都还没支持),驱动都得改(虽然改动量应该不大)
CI/CD workflow - github copilot
到此也算开发完了,我就让copilot cli把这个项目推到github了,然后直接在网页的agents里让copilot去弄CI(毕竟是github自己的能力,就让github自己干了)。另外,我前面用的jdbc包在网上没有下载,我就另外给了个旧版本可以下载地址


后来我又发现崖山刚刚把jdbc驱动上传到maven了,就又改了下

action里直接跑release,也自动帮我打包发布了

性能测试 - Qoder
开发完了,自然要做下性能对比测试,我看了下我qoder还没用完,就让qoder写了个测试

的确有发现性能问题,其他场景性能表现正常,但批量插入的性能慢了几十倍

于是就让qoder去克隆了这个项目找问题

Qoder说找到了BUG,我就转给了copilot,自动修去了

修完再测
性能对比结果
测试环境: YashanDB 23.2, JDK 17, 10 轮迭代
吞吐量对比 (ops/sec)
| 测试场景 | JDBC | R2DBC | 胜者 |
|---|---|---|---|
| Simple Query | 1127.96 | 1146.50 | R2DBC |
| Batch Query (1K rows) | - | - | 平局 |
| Batch Query (10K rows) | - | - | 平局 |
| Simple Insert | 1109.88 | 1139.64 | R2DBC |
| Batch Insert (100 rows) | 53.15 | 84.83 | R2DBC |
| Concurrent Query (10 threads) | 1109.88 | 1109.16 | 平局 |
| Concurrent Query (50 threads) | 2548.80 | 2588.96 | R2DBC |
| Concurrent Insert (10 threads) | 95.97 | 92.86 | JDBC |
| Concurrent Insert (50 threads) | 124.25 | 170.00 | R2DBC |
| Transaction | 3.25 | 3.23 | 平局 |
延迟对比 (ms)
| 测试场景 | JDBC Avg | R2DBC Avg | JDBC P95 | R2DBC P95 |
|---|---|---|---|---|
| Simple Query | 88.67 | 87.22 | 93.48 | 92.41 |
| Batch Query (1K) | 201.60 | 194.81 | 213.05 | 206.93 |
| Batch Query (10K) | 878.61 | 863.48 | 920.68 | 912.70 |
| Simple Insert | 89.98 | 87.67 | 96.69 | 96.74 |
| Batch Insert (100) | 188.15 | 117.88 | 194.55 | 141.93 |
| Concurrent Insert (50) | 313.68 | 224.35 | 568.77 | 285.07 |
结论
| 统计 | 数量 |
|---|---|
| JDBC 胜出 | 1 |
| R2DBC 胜出 | 4 |
| 平局 | 5 |
测试程序源码:https://github.com/Dark-Athena/yashandb-r2dbc-vs-jdbc-test
总结
这体验简直是飞起来了,全程我一行代码没看,一行代码没写,甚至连git命令都没敲,就开发出来了能跑通R2DBC的官方测试的崖山数据库R2DBC驱动,CI/CD也齐活了。按以往,这种项目是要一个团队去维护的(当然,高级工程师自己一个人也能搞定)
项目开源地址 https://github.com/Dark-Athena/r2dbc-yashandb
本次开销 CodeBuddy 600 Credits, copilot使用都不到$0.5, Qoder用了 710 Credits(这玩意烧得贼快...)。重点是,我发布任务后不需要盯着看,自己可以去干其他活,隔一段时间回来看看就行(其实也不用,现在很多AI CODING工具,任务完成了会有音效提醒的),也就是说时间成本几乎可以忽略不计了。这意味着真的可以一个人同时替代多个团队了!
下一篇就是使用这个新开发的R2DBC驱动,来连接HALO博客和崖山数据库了。
