

过去做数据库适配时,我最常见的做法是修改 JDBC 驱动和连接地址,应用能够启动、
SELECT 1可以执行,就先把"数据库已适配"打上勾.但真正把业务代码跑起来后才会发现,连接成功只是第一关:主键生成方式、分页语法、保留字、标识符大小写、事务回滚以及并发更新,都可能在后面埋坑.这次我选择了一个更贴近实际开发的场景:在M4 Mac上运行 KingbaseES,用Spring Boot 3.5 + MyBatis 3.0实现商品库存和订单接口.除了完成基本CRUD,我还特意制造了库存不足和并发抢购两个失败场景,检查订单能否正确回滚,以及库存会不会被扣成负数.整个过程并非一路顺利.我原以为M4需要通过AMD64模拟运行数据库,实际却在官网下载到了原生 aarch64 镜像;按照平时的习惯启动容器,又在日志中碰到了权限告警;把MySQL DDL 直接搬过来时,AUTO_INCREMENT、保留字和大小写问题也接连出现.本文记录的就是这些真实操作、排查过程和解决办法,希望能给正在进行国产数据库适配的Java开发者提供一份可以直接参考的实测记录.


目录
一、我想验证的,不只是"能不能连上"
接触一个不熟悉的数据库时,最容易写出的文章是:建一个 Spring Boot 项目,改四行数据源配置,执行一条 SELECT 1,然后宣布适配完成.但做过业务系统迁移的人都知道,连接成功只是起点.真正容易出问题的地方,往往藏在建表语法、主键回填、分页、保留字、事务边界和并发更新里.
所以这次我没有做"Hello World",而是给自己定了一个更接近真实需求的小题目:用Spring Boot + MyBatis写一套商品库存和订单接口,覆盖商品的增删改查,再验证两个关键场景:订单写入后扣库存失败,订单能否跟着回滚;两个请求同时抢库存时,会不会扣成负数.
我原先还有一个判断:M4是ARM架构,可能只能拉 x86 镜像后用模拟运行.实际下载时这个判断就被推翻了——金仓官网已经提供 aarch64 Docker 包.这个小插曲也提醒我,数据库适配不要凭过去的印象写方案,先以当前版本的官方介质为准.
二、环境准备:M4可以直接跑ARM64版本
我的本机环境如下.JDK选择17,是因为本文使用Spring Boot 3.5.x;Docker 服务端本身也是 arm64.
Hardware: Apple M4 / arm64
macOS: 26.6
Docker Engine: 29.6.2 / linux-arm64
Java: OpenJDK 17.0.20
Maven: 3.9.16


我从金仓数据库官网下载中心取得两个文件:
KingbaseES_V009R001C010B0004_aarch64_Docker.tarKingbaseES_V009R001C010B0004_JDBC.zip
这里要留意,Docker 数据库镜像和 JDBC 驱动是两个独立下载项,不能假设镜像包里一定带着应用侧要用的 jar.下载后我先校验了官网给出的 MD5,再加载镜像:
md5 KingbaseES_V009R001C010B0004_aarch64_Docker.tar
md5 KingbaseES_V009R001C010B0004_JDBC.zip
docker load -i KingbaseES_V009R001C010B0004_aarch64_Docker.tar
docker image inspect kingbase_v009r001c010b0004_single_arm:v1 \
--format '{{.Os}}/{{.Architecture}}'
最后一条返回 linux/arm64,不是通过 Rosetta 或 QEMU 跑起来的 AMD64 镜像.对M系列Mac 来说,少一层模拟,启动速度和资源占用都更踏实.
三、第一次启动的小坑:容器能跑,不代表启动参数规范
我第一次照着常见数据库容器的方式启动,没有加 --privileged.数据库最终虽然起来了,日志中却出现:
sudo: pam_open_session: Permission denied
这类告警很容易被"数据库已经能连接"掩盖.我回看官方 Docker 安装手册,重新按容器运行要求创建,并把宿主机 54321 映射到容器 54321:
docker run -d \
--name kes-v9-dev \
--privileged \
-p 54321:54321 \
-e DB_MODE=pg \
-e DB_USER=system \
-e DB_PASSWORD='<初始化强密码>' \
-e NEED_START=yes \
-v '<本机数据目录>:/home/kingbase/userdata' \
kingbase_v009r001c010b0004_single_arm:v1
DB_MODE=pg 表示这次按 PG 兼容模式验证.挂载目录不要随便指向临时路径,否则删容器后测试数据也一起没了.口令也不要写进 Git、文章附件或镜像层,本文后面的应用配置统一从环境变量读取.

重新启动后,日志出现 server started.我又进入容器核对架构和客户端版本,结果分别是 aarch64 与 ksql (KingbaseES) V009R001C010.


接着创建独立业务用户和数据库.开发项目不要长期拿初始化管理员账号直连,哪怕只是本机演示,也应把这个习惯保留下来.
CREATE USER order_app WITH PASSWORD '<业务账号强密码>';
CREATE DATABASE order_demo OWNER order_app;
四、表结构先适配:别把MySQL DDL原封不动搬过来
示例只有两张表:product_stock 保存可用库存,purchase_order 保存订单.主键使用标准的 identity 写法,库存和数量则用 CHECK 保住数据底线.
CREATE TABLE product_stock (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
sku VARCHAR(64) NOT NULL UNIQUE,
product_name VARCHAR(128) NOT NULL,
available_stock INTEGER NOT NULL CHECK (available_stock >= 0),
version INTEGER NOT NULL DEFAULT 0,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE purchase_order (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
order_no VARCHAR(64) NOT NULL UNIQUE,
sku VARCHAR(64) NOT NULL,
quantity INTEGER NOT NULL CHECK (quantity > 0),
status VARCHAR(32) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_purchase_order_sku ON purchase_order (sku);
这里我没有给两表加外键.不是说外键不能用,而是订单记录往往要保留业务发生时的 SKU,即使商品后来下架也不能让历史订单消失.是否加外键应该服从业务生命周期,不能为了让演示"看起来规范"硬绑关系.
version 字段也要解释清楚:本文只是让它随库存变化递增,方便观察更新次数;真正防止超卖的是后面那条带库存条件的原子 UPDATE,不能把这个字段包装成已经实现了完整的乐观锁.
五、接入JDBC:我踩到的不是代码坑,而是依赖来源坑
按惯性在 Maven Central 中写一个驱动坐标,很可能得到依赖不存在的错误.我实际检查时,com.kingbase8:kingbase8:9.0.0 并不能从 Maven Central 直接取得.解决方法是使用官网下载的 JDBC 包,并把 jar 安装到本机 Maven 仓库;团队环境则更适合上传到公司 Nexus/Artifactory,统一管理版本.
mvn install:install-file \
-Dfile=kingbase8-9.0.0.jar \
-DgroupId=com.kingbase8 \
-DartifactId=kingbase8 \
-Dversion=9.0.0 \
-Dpackaging=jar
pom.xml 的核心依赖如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.16</version>
</parent>
<properties>
<java.version>17</java.version>
<mybatis-spring-boot.version>3.0.5</mybatis-spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-spring-boot.version}</version>
</dependency>
<dependency>
<groupId>com.kingbase8</groupId>
<artifactId>kingbase8</artifactId>
<version>9.0.0</version>
</dependency>
</dependencies>
Spring Boot 与 MyBatis 的版本并不是随手拼的.Spring Boot 3.5.x 要求至少 Java 17;MyBatis Spring Boot Starter 3.0 系列覆盖 Boot 3.2~3.5 和 Java 17 及以上.跨数据库适配时,先把框架版本关系固定住,能避免把框架兼容问题误诊成数据库问题.
六、数据源配置:驱动类和URL都要换,密码不要落盘
我的 application.yml 如下:
spring:
datasource:
url: jdbc:kingbase8://localhost:54321/order_demo
username: ${KES_USERNAME:order_app}
password: ${KES_PASSWORD}
driver-class-name: com.kingbase8.Driver
hikari:
maximum-pool-size: 5
minimum-idle: 1
connection-timeout: 3000
connection-test-query: SELECT 1
mybatis:
mapper-locations: classpath:/mapper/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
启动前用环境变量传入口令:
export KES_USERNAME=order_app
export KES_PASSWORD='<业务账号强密码>'
mvn spring-boot:run
一开始我只看见 Spring Boot 正常启动,还不敢把它算作验证通过,于是专门写了一个 /api/database/info 接口,从当前连接查询数据库版本、数据库名和用户.实际返回的是 KingbaseES V009R001C010、order_demo、order_app.这一步能排除“应用其实连到了本机另一个 PostgreSQL 实例”之类的低级误判.


七、CRUD核心:主键回填、分页和安全更新都走一遍
MyBatis 接口没有特殊注解技巧,真正决定适配性的仍然是SQL.新增商品使用 identity 主键,并让驱动把生成的 id 回填到 Java 对象:

<insert id="insert" useGeneratedKeys="true" keyProperty="id">
INSERT INTO product_stock (sku, product_name, available_stock)
VALUES (#{sku}, #{productName}, #{availableStock})
</insert>
<select id="selectPage" resultType="com.example.kingbase.domain.ProductStock">
SELECT id, sku, product_name, available_stock, version, updated_at
FROM product_stock
ORDER BY id
LIMIT #{limit} OFFSET #{offset}
</select>
<update id="adjustStock">
UPDATE product_stock
SET available_stock = available_stock + #{delta},
version = version + 1,
updated_at = CURRENT_TIMESTAMP
WHERE sku = #{sku}
AND available_stock + #{delta} >= 0
</update>
<delete id="deleteBySku">
DELETE FROM product_stock WHERE sku = #{sku}
</delete>
我给 REST 层准备了五组动作:新增商品、按 SKU 查询、分页查询、调整库存、删除商品.page 和 size 没有直接原样传给 SQL:页码最小为1,每页限制在 1~100 之间,再计算 OFFSET.这不是金仓独有的要求,而是换库时很容易顺手遗漏的输入边界.
# 新增
curl -X POST http://localhost:8080/api/products \
-H 'Content-Type: application/json' \
-d '{"sku":"KB-DEMO-001","productName":"金仓数据库实战课","initialStock":10}'
# 加 5 个库存
curl -X PATCH http://localhost:8080/api/products/KB-DEMO-001/stock \
-H 'Content-Type: application/json' \
-d '{"delta":5}'
# 分页、删除
curl 'http://localhost:8080/api/products?page=1&size=20'
curl -X DELETE http://localhost:8080/api/products/KB-TEMP-001
实测中,首条商品返回 id=1,说明生成主键成功回填;库存从10调到15,version 从0变为1;临时商品删除返回 204,再查询返回 404;重复SKU被统一映射为409,没有把长串数据库异常直接甩给前端.

到这里可以确认普通 CRUD 没问题,但这仍然只完成了"基本可用".对订单系统来说,下面两项才是我最关心的.
八、事务实测:先写订单、后扣库存,失败时能否一起撤销
订单创建故意采用"先插入订单,再扣库存"的顺序.这样只要扣减失败,最容易暴露事务是否真的生效.核心服务代码如下:
@Service
public class OrderService {
private final PurchaseOrderMapper purchaseOrderMapper;
private final ProductStockMapper productStockMapper;
public OrderService(PurchaseOrderMapper purchaseOrderMapper,
ProductStockMapper productStockMapper) {
this.purchaseOrderMapper = purchaseOrderMapper;
this.productStockMapper = productStockMapper;
}
@Transactional
public PurchaseOrder create(CreateOrderRequest request) {
PurchaseOrder order = new PurchaseOrder();
order.setOrderNo(request.orderNo());
order.setSku(request.sku());
order.setQuantity(request.quantity());
order.setStatus("CREATED");
purchaseOrderMapper.insert(order);
int affected = productStockMapper.deductStock(
request.sku(), request.quantity());
if (affected != 1) {
throw new AppException(
HttpStatus.CONFLICT, "库存不足或商品不存在");
}
return requireByOrderNo(request.orderNo());
}
}
扣库存不是"先查余额,再在Java里判断,再更新".那种写法在并发下有时间窗口.我把条件放进同一条 SQL:
<update id="deductStock">
UPDATE product_stock
SET available_stock = available_stock - #{quantity},
version = version + 1,
updated_at = CURRENT_TIMESTAMP
WHERE sku = #{sku}
AND available_stock >= #{quantity}
</update>
先用数量 3 创建订单,HTTP 返回 201,库存由15变为12.随后用数量99创建 ORD-ROLLBACK-001,接口返回 409.关键不是报错本身,而是我再进数据库查:该订单数量为 0,库存仍是 12.说明前面已经执行的 INSERT 确实随运行时异常回滚了,Spring 事务管理器、JDBC 驱动和 KingbaseES 的事务协作符合预期.

这里还有一个常见误区:把 @Transactional 加在同类内部调用的方法上,然后从另一个普通方法用 this.create() 调它.此时可能绕过 Spring 代理,注解看着在,事务却没进去.我的事务方法放在独立 @Service 的 public 方法上,由 Controller 经 Spring Bean 调用,避免了自调用失效.
九、并发实测:10个库存,同时来两个"买7个"
事务回滚通过后,我把库存重置为10,同时发出两笔各买7个的请求.理论上最多只能成功一笔;如果代码存在"先查后改"的竞态,两笔都可能读到10,最终产生超卖.

curl -s -o /tmp/order-a.json -w '%{http_code}' \
-X POST http://localhost:8080/api/orders \
-H 'Content-Type: application/json' \
-d '{"orderNo":"ORD-CONCURRENT-A","sku":"KB-DEMO-001","quantity":7}' &
curl -s -o /tmp/order-b.json -w '%{http_code}' \
-X POST http://localhost:8080/api/orders \
-H 'Content-Type: application/json' \
-d '{"orderNo":"ORD-CONCURRENT-B","sku":"KB-DEMO-001","quantity":7}' &
wait
实测结果是一笔 201、一笔 409;数据库里只留下成功订单,最终库存为3,没有负数,也没有失败订单残留.具体是哪一笔成功并不重要,调度顺序本来就不应成为业务假设.


这个方案依赖数据库对单条条件更新的原子性,简单而有效.如果业务还要处理多 SKU 锁定、限时释放、跨服务消息等场景,就需要继续设计锁顺序、幂等键和补偿机制;不能因为这个小测试通过,就推导出所有并发问题都解决了.
十、三个迁移时很容易撞上的SQL坑
为了确认兼容边界,我没有只跑正确 SQL,还把几段常见的迁移语句故意送进数据库.

1. AUTO_INCREMENT不能照搬
在本次 PG 兼容模式下执行:
CREATE TABLE mysql_style (
id BIGINT AUTO_INCREMENT PRIMARY KEY
);
数据库在 AUTO_INCREMENT 附近报语法错误.我的处理是改成 GENERATED BY DEFAULT AS IDENTITY.如果从MySQL迁移,除了 DDL,还要全量检查 ON DUPLICATE KEY UPDATE、反引号、无符号类型、时间函数等方言,不要等应用上线后逐条踩雷.
2. order是保留字
CREATE TABLE order (...) 直接报错.可以写成带双引号的 "order",但之后每条 SQL 都得正确引用,维护成本很高.我最终把表名改成 purchase_order.数据库迁移里,"改一个不冲突的业务名"通常比"到处加引号"稳妥.
3. 未加引号的标识符会折叠为小写
我创建了 "CaseDemo",再执行不带引号的 SELECT * FROM CaseDemo,实际查找的是 casedemo,于是报关系不存在;写成 SELECT * FROM "CaseDemo" 才成功.解决办法不是要求团队记住每一个大小写,而是从建表开始统一使用小写蛇形命名,MyBatis 再通过 map-underscore-to-camel-case 映射为 Java 驼峰字段.

这三项都不是"数据库不好用",而是源库方言和目标库规则不同.有效的迁移流程应该先扫描对象和 SQL,再做兼容改写,最后用回归测试验证,而不是把连接串换完就上线.
十一、我是怎样判断这次适配真的完成了
最终我给自己列了一张比"应用启动成功"更严格的验收表:
| 检查项 | 实测结果 | 关注点 |
|---|---|---|
| 原生架构 | 通过 | 镜像与容器均为 aarch64 |
| JDBC 连接 | 通过 | 返回 KingbaseES 版本、业务库和业务用户 |
| 新增与主键回填 | 通过 | identity 主键回填 id=1 |
| 查询与分页 | 通过 | LIMIT/OFFSET 正常,参数有限界 |
| 更新与删除 | 通过 | 库存不降为负数,删除后返回 404 |
| 唯一键异常 | 通过 | 重复 SKU 转为 409,未暴露内部异常 |
| 本地事务 | 通过 | 扣库存失败后订单行回滚 |
| 并发扣减 | 通过 | 10 个库存下两笔 7 个仅一笔成功 |
| 构建与上下文测试 | 通过 | 1 个测试,0 失败、0 错误 |
项目最后执行 mvn test,Spring 上下文能够用 JDK 17 启动,测试结果为 Tests run: 1, Failures: 0, Errors: 0,构建成功.

如果要把这个示例推进到生产,我还会补四件事:
第一,用 Flyway 或 Liquibase 管理版本化 DDL,而不是人工执行脚本;
第二,在测试环境用Testcontainers 或专用 KingbaseES 实例跑集成测试;
第三,根据压测结果设置连接池、慢 SQL 与超时,不照抄本文的5个连接;
第四,梳理原系统的数据库特有语法、存储过程和类型映射,建立可重复执行的兼容清单.
十二、回头看:框架改动不大,验证方式才是重点
这次实测给我的结论很朴素:对于采用常规 SQL 的 Spring Boot + MyBatis 项目,接入 KingbaseES 的 Java 代码改动并不大,主要变化集中在官方 JDBC 驱动、连接 URL 和目标库方言.真正花时间的地方,不是把数据源配上,而是确认那些"以前默认成立"的事情在新数据库上仍然成立.
比如,M4 是否只能模拟 x86,要用镜像架构回答;主键能不能回填,要看新增接口返回;事务有没有生效,要制造一次中途失败再查库;并发会不会超卖,要让两个请求真的撞在一起.把这些问题变成可观察、可复现的小实验,数据库适配才从"我觉得可以"变成"我验证过可以".
这也是我认为国产数据库适配最值得保留的方法:少一点根据相似性做推断,多一点以官方介质、真实 SQL 和失败场景为证据.连接成功值得高兴,但敢于主动制造失败,才更接近生产可用.
参考资料
- KingbaseES 官网下载中心
- KingbaseES Docker 安装手册
- KingbaseES JDBC 使用文档
- Spring Boot 3.5 系统要求
- MyBatis Spring Boot Starter 官方说明

敬请期待下一篇文章内容
每日心灵鸡汤: 当你开始害怕别人不开心,其实是在保护过去的自己!
当你因为他人语气不好、态度不耐烦而下意识紧张、自我怀疑时,这往往不是当下的问题,而是过去经验留下的自动反应.你曾经为了在不稳定的环境中保护自己,学会把别人的情绪当作一种"危险信号",于是通过讨好、压抑来换取安全感.但这种机制只是童年时期的生存策略,并不适用于现在的你.真正重要的是意识到:当下的你已经有能力区分现实与过去,不需要再用过度敏感来保护自己.别人的情绪属于他们自己,你不需要为此负责;你只需要稳住自己,专注于自己的感受与选择.

转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2401_87629362/article/details/163593545




