目 录CONTENT

文章目录

【YaShanDB】给YaShanDB开发R2DBC驱动

DarkAthena
2026-03-28 / 0 评论 / 0 点赞 / 2 阅读 / 0 字

【YaShanDB】使用AI给YaShanDB开发R2DBC驱动

背景

崖山数据库作为一款发布没多少年的纯自研数据库,在内核设计上非常亮眼,而且也通过了国测,但是周边的应用生态还有不少欠缺。最近打算让HALO博客的后台支持崖山数据库,但是却发现崖山数据库官方竟然没有R2DBC驱动,就想着看能不能给崖山做一个。

AI CODING - CodeBuddy

前几天在CodeBuddy领了免费的1000 credits,正好用来试试看,毕竟也算稍微大一点的工程,我可不舍得烧copilot。
使用PLAN模式,模型Default,CodeBuddy会自动分析需求,获取所有必须的知识,然后起草计划,问我是否按这个计划开始执行

我打算开发yashandb的r2dbc驱动,但我手上只有yashandb的jdbc驱动,你排个计划,看怎么做。jdbc驱动jar文件我已经放到当前目录了

image-VqoY.png

image-RUKZ.png


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。

核心功能

  • 连接管理:实现 ConnectionFactoryConnection SPI,支持 r2dbc:yashandb:// URL 格式,连接池友好
  • SQL 执行:实现 Statement SPI,支持带参数绑定的 DML/DDL/DQL 语句执行
  • 结果集处理:实现 ResultRow/RowMetadata SPI,支持 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,可直接复用。整体思路:

  1. 复用协议层:直接使用 NativeProtocolYasSocketConnectionPacketExecuteResult 等底层类进行网络通信与结果读取,避免重复实现协议解析。
  2. 复用配置层:使用 com.yashandb.conf.ConnectionUrl 解析 R2DBC URL,YasProperty 读取连接属性。
  3. 复用类型层:使用 com.yashandb.protocol.accessor.* 30+ 类型访问器 + com.yashandb.core.DataType 枚举完成 SQL 类型到 Java 类型的双向映射。
  4. 异步调度:由于 YashanDB 底层 Socket IO 是同步阻塞的,所有阻塞调用统一通过 Mono.fromCallable(...).subscribeOn(Schedulers.boundedElastic()) 包装,确保不阻塞 Reactor 事件循环线程。
  5. 背压支持Result.map() 返回 Flux<T>,逐行发射,支持 Reactor 背压。

关键技术决策

  • 本地 jar 引入:通过 Maven system scope 或安装到本地 .m2 仓库(推荐后者,避免路径耦合),pom.xml 配置 <scope>compile</scope>
  • R2DBC URL 格式r2dbc:yashandb://user:password@host:port/databaseYashanDbConnectionFactoryProvider 负责解析并委托给 ConnectionUrl
  • 连接状态机YashanDbConnection 维护 autoCommittransactionStatus 枚举状态,事务操作返回 Mono<Void>
  • 线程安全:Protocol 层连接对象不保证线程安全,通过 AtomicBoolean closed + Mono.defer 串行化操作,避免并发问题。
  • 错误转换:将 com.yashandb.* 异常统一转换为 R2dbcException 子类(R2dbcBadGrammarExceptionR2dbcNonTransientResourceException 等)。

架构设计

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] 单元测试:各类型编解码正确性

关键代码结构

YashanDbConnectionConfiguration Builder

public 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 文件结构,精确定位 NativeProtocolYasSocketConnectionExecuteResultConnectionUrlYasProperty 等关键类的方法签名,为驱动实现提供准确的 API 调用依据。
  • 预期产出:获得协议层关键类的构造方法、核心方法签名,确保驱动实现代码能正确调用 JDBC jar 中的 internal API,避免编译错误。错误。

后面他就啪啪啪自己在干了,我也去干其他活了,没管,烧了大概250 Credits后就开发完了,他自己编写的测试用例也通过了。

不过由于没有提前给他可用的崖山数据库服务端环境,因此他的测试没有真实连库,于是我给了个环境给他,然后他又自己去干了,测了不对他就自己改,直到他认为本次任务已完成

image-ifow.png

image-GjfA.png

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

image-XQjz.png

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

image-BXqV.png

很明显这里列名大小写他幻觉了。肯定有很多开发人员没搞懂数据库字段名大小写的原理,因此把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包在网上没有下载,我就另外给了个旧版本可以下载地址

image-xKQK.png

image-VUvu.png

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

image-ALrM.png

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

image-NJrz.png

性能测试 - Qoder

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

image-imff.png

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

image-ZSIT.png

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

image-bEXO.png

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

image-rSXy.png

修完再测

性能对比结果

测试环境: YashanDB 23.2, JDK 17, 10 轮迭代

吞吐量对比 (ops/sec)

测试场景JDBCR2DBC胜者
Simple Query1127.961146.50R2DBC
Batch Query (1K rows)--平局
Batch Query (10K rows)--平局
Simple Insert1109.881139.64R2DBC
Batch Insert (100 rows)53.1584.83R2DBC
Concurrent Query (10 threads)1109.881109.16平局
Concurrent Query (50 threads)2548.802588.96R2DBC
Concurrent Insert (10 threads)95.9792.86JDBC
Concurrent Insert (50 threads)124.25170.00R2DBC
Transaction3.253.23平局

延迟对比 (ms)

测试场景JDBC AvgR2DBC AvgJDBC P95R2DBC P95
Simple Query88.6787.2293.4892.41
Batch Query (1K)201.60194.81213.05206.93
Batch Query (10K)878.61863.48920.68912.70
Simple Insert89.9887.6796.6996.74
Batch Insert (100)188.15117.88194.55141.93
Concurrent Insert (50)313.68224.35568.77285.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博客和崖山数据库了。

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin
博主关闭了所有页面的评论