承渊政道头像
关注
【金仓数据库征文】M4 Mac上把Spring Boot + MyBatis接到KingbaseES:一次带事务和并发扣库存的实测封面图

【金仓数据库征文】M4 Mac上把Spring Boot + MyBatis接到KingbaseES:一次带事务和并发扣库存的实测

🔥承渊政道:个人主页

❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》 《MySQL数据库学习》 《Python知识内容》

✨逆境不吐心中苦,顺境不忘来时路!✨
🎬 博主简介:

过去做数据库适配时,我最常见的做法是修改 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.tar
  • KingbaseES_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.我又进入容器核对架构和客户端版本,结果分别是 aarch64ksql (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 V009R001C010order_demoorder_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 查询、分页查询、调整库存、删除商品.pagesize 没有直接原样传给 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 和失败场景为证据.连接成功值得高兴,但敢于主动制造失败,才更接近生产可用.


参考资料

  1. KingbaseES 官网下载中心
  2. KingbaseES Docker 安装手册
  3. KingbaseES JDBC 使用文档
  4. Spring Boot 3.5 系统要求
  5. MyBatis Spring Boot Starter 官方说明


🚀真正的勇者不是流泪的人,而是含泪奔跑的人!

敬请期待下一篇文章内容


每日心灵鸡汤: 当你开始害怕别人不开心,其实是在保护过去的自己!

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

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2401_87629362/article/details/163593545

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--