前言:一个要"原样跑起来"的新项目
今年年初,我们组接了个内部交易中台的重构活儿。架构早就定了,Spring Boot + MyBatis + Druid,数据库这次要换成金仓数据库。组里大部分人之前都是写 Oracle 上的应用,对金仓不算熟。leader 把数据库接入这块甩给了我,原话是:"先把连接打通,存储过程跑顺,别出幺蛾子。"
我当时答应得挺痛快,心想不就换个数据库嘛。真上手才知道,"打通"这俩字背后全是坑。这篇就把我在驱动、连接池、MyBatis、存储过程调试这几处栽过的跟头记一下,给后来人提个醒。
目录
-
- 前言:一个要"原样跑起来"的新项目
- 痛点:看似只是换个数据库,实则处处暗礁
- [方案:JDBC + Druid + MyBatis + KStudio 调试](#方案:JDBC + Druid + MyBatis + KStudio 调试)
- 踩坑实录
-
- [坑一:JDBC 驱动 jar 跟 JDK 版本没对上](#坑一:JDBC 驱动 jar 跟 JDK 版本没对上)
- [坑二:Druid 连接池"假活",连接泄漏](#坑二:Druid 连接池"假活",连接泄漏)
- [坑三:MyBatis 里的 Oracle 方言 SQL](#坑三:MyBatis 里的 Oracle 方言 SQL)
- [坑四:存储过程跑不对,靠 KStudio 单步揪出 bug](#坑四:存储过程跑不对,靠 KStudio 单步揪出 bug)
- [关键优化:大结果集的 fetchsize](#关键优化:大结果集的 fetchsize)
- 落地效果
- 写在最后
痛点:看似只是换个数据库,实则处处暗礁
动手前我盘了一下,麻烦主要在三处。
一是驱动。金仓的 JDBC 驱动按 JDK 版本分了好几个 jar,跟平时用 MySQL 那种一个 jar 走天下完全不一样。版本没对上,启动直接类加载失败,连数据库的影儿都摸不着。
二是连接池。原来 Druid 配的是针对另一套库的参数,验证语句、超时这些照搬过来,连接池看着是活的,其实是"假活",线上隔三差五就报错,排查起来特别费劲。
三是存储过程。业务里有一批 PL/SQL 存储过程,迁过来逻辑跑不对。光盯着代码看,看不出来毛病,得靠调试一步步过。
方案:JDBC + Druid + MyBatis + KStudio 调试
技术栈没得选,就是那套。JDBC 用金仓原生驱动,连接池继续 Druid,ORM 还是 MyBatis,存储过程调试用金仓自带的 KStudio。
整体思路其实挺简单:先把连接跑通,再调连接池参数,接着把 MyBatis 里的方言 SQL 适配掉,最后用 KStudio 把存储过程的 bug 一个个揪出来。听起来就四步,实际每一步都够喝一壶。我画了张图,照着这个顺序往下啃:

踩坑实录
坑一:JDBC 驱动 jar 跟 JDK 版本没对上
第一个坑来得特别快。周一早上,我把驱动 jar 往项目里一丢,启动,直接报 ClassNotFoundException: com.kingbase8.Driver。
第一反应是 jar 没引进来。检查了半天 pom 和 lib 目录,jar 明明就躺在那儿。我又怀疑是 IDE 缓存,清缓存重启,还是一样。卡了快一个小时,后来翻了金仓的文档才反应过来--它的 JDBC 驱动是按 JDK 版本分的。我项目跑 JDK8,结果手滑丢进去的是 jre6 那个。版本不匹配,类加载直接挂,怪不得报找不到类。
| 驱动 jar 文件 | 对应 JDK 版本 |
|---|---|
| kingbase8-9.0.0.jre6.jar | JDK 1.6 |
| kingbase8-9.0.0.jre7.jar | JDK 1.7 |
| kingbase8-9.0.0.jar | JDK 1.8 |
这些 jar 都在数据库安装目录的 Interface/jdbc 下。别想当然随便抓一个就塞进去,先看清楚自己项目的 JDK 版本。换回对应版本,连接立马通了:
java
// 金仓 JDBC 连接:注意驱动类名和连接串前缀都跟 Oracle/MySQL 不一样
Class.forName("com.kingbase8.Driver");
String url = "jdbc:kingbase8://10.0.0.12:54321/trade"
+ "?currentSchema=trade";
try (Connection conn = DriverManager.getConnection(url, "appuser", "pwd123")) {
System.out.println("连通了,版本: " + conn.getMetaData().getDatabaseProductVersion());
}
这种坑说实话挺低级,但越是低级的坑,越容易因为"这还能有错"的心态忽略掉。
坑二:Druid 连接池"假活",连接泄漏
这个坑折磨了我小两天。
应用本地跑起来一切正常,压测也没事。可一上预发,隔三差五就报"无法获取连接"。看日志,连接池里的连接全被占满了。重启就好,过会儿又犯,跟闹钟似的。
我第一反应是连接没关。把代码里所有 getConnection 翻了一遍,都老老实实 try-with-resources 了,没漏。这就卡住了--代码没问题,连接池却满了,邪门。
后来盯 Druid 的监控页面才看出门道。连接池里有大量连接处于"活动"状态,但实际上对数据库已经失效了。问题出在 validationQuery 上。原来这套配置是从别的库照搬的,验证语句写的是那套库的写法,到金仓这儿语义对不上,等于没验证。连接早断了,池子还以为它活着,业务一拿就是个死连接,占着茅坑不拉屎,很快就被耗光了。
排查这玩意儿我走了弯路,后来总结了个流程,照着走能少绕不少弯子:

光看池子不行,我还直接去数据库里数了一把活动会话,确认是不是真的有一堆僵死连接赖在那儿:
sql
-- 直连金仓,查当前活动会话,看有没有一堆连接赖着不动
SELECT pid, usename, application_name, client_addr,
state, query_start, state_change,
LEFT(query, 60) AS query
FROM sys_stat_activity
WHERE datname = 'trade'
ORDER BY query_start DESC;
一查果然,一堆 state = idle 的连接,state_change 时间还是十几分钟前的,典型的僵死连接。证据确凿,改配置就有了方向。把验证语句换成金仓能识别的,再配上 testWhileIdle,问题立马消停:
yaml
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.kingbase8.Driver
url: jdbc:kingbase8://10.0.0.12:54321/trade?currentSchema=trade
username: appuser
password: pwd123
druid:
# 关键:验证语句得是金仓认的,别照搬别家的
validation-query: SELECT 1
test-while-idle: true
test-on-borrow: false
test-on-return: false
# 空闲连接存活检测间隔,别让死连接赖在池子里
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
max-active: 50
min-idle: 5
顺带说一句,金仓兼容模式下 SELECT 1 FROM DUAL 也能跑,但直接 SELECT 1 更省事,没必要绕那一下。
坑三:MyBatis 里的 Oracle 方言 SQL
MyBatis 接入本身不难。驱动类名、URL 换掉,配置基本就齐了。金仓对 MyBatis 3.x 的几个版本都做过适配验证,这块挺省心。
properties
# jdbc.properties
jdbc.driverClassName=com.kingbase8.Driver
jdbc.url=jdbc:kingbase8://10.0.0.12:54321/trade?currentSchema=trade
jdbc.username=appuser
jdbc.password=pwd123
xml
<!-- mybatis config.xml -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
真正的坑不在配置,在 Mapper 的 SQL 里。原来有一段拿主键的逻辑,直接调了 Oracle 的序列:
xml
<!-- 原写法:直接调序列 nextval,Oracle 上没毛病 -->
<select id="nextOrderId" resultType="long">
SELECT seq_order.nextval FROM DUAL
</select>
金仓兼容模式下序列和 DUAL 都支持,这段能跑。但我心里不踏实--这种方言写法留在代码里,以后就是个隐患,指不定哪天换个驱动版本、换个模式就炸了。索性趁这次接入,把拿主键的方式统一换掉。新表直接用 IDENTITY 列,插入时用 useGeneratedKeys 一把拿回主键,干净利落,跟序列彻底说再见:
xml
<!-- 改造后:用 RETURNING 直接拿自增主键,跟序列说再见 -->
<insert id="insertOrder" parameterType="Order" useGeneratedKeys="true" keyProperty="id">
INSERT INTO orders(order_no, amount, create_time)
VALUES(#{orderNo}, #{amount}, #{createTime})
</insert>
我的建议是,接入的时候顺手把这类方言写法扫一遍,能换的就换掉,别图省事留着。当时省的事,以后都得还。
坑四:存储过程跑不对,靠 KStudio 单步揪出 bug
前面几个坑好歹是应用层的,看得见摸得着。存储过程这个就恶心了。
有个算订单优惠金额的存储过程,迁过来以后个别订单算出来的结果跟预期差几分钱。光盯着代码看,逻辑好像没毛病,变量赋值、循环、条件分支,都对得上。瞪了半天眼睛,没看出问题。
没办法,上 KStudio 调试。这工具是金仓自带的图形界面,能对 PL/SQL 函数和存储过程断点单步调试,比光看代码硬啃强太多。操作不复杂,我后来画了张图,照着点就行:

具体操作就是:在对象树里找到那个函数,右键选"调试",把入参填进去,点"开始调试"。然后就是工具栏那几个按钮--"单步跳过"一步步往下走,"单步跳入"钻进嵌套调用的函数里。变量值实时显示在旁边,哪一步算错了看得一清二楚。
调了两轮,bug 现形了。是中间一步 SELECT ... INTO 取折扣率的时候,没处理空值。遇到某类没配折扣的商品,INTO 取回来是空,后续乘法一算,结果就飘了。代码大概长这样:
sql
-- 出问题的存储过程片段:INTO 没兜住空值
CREATE OR REPLACE PROCEDURE calc_order_amount(p_order_id IN NUMERIC) AS
v_discount NUMERIC;
v_amount NUMERIC;
BEGIN
-- 这里:某些商品没配折扣,discount 取回来是 NULL
SELECT discount_rate INTO v_discount
FROM product_rule WHERE product_id = (SELECT product_id FROM orders WHERE id = p_order_id);
-- NULL 参与乘法,结果直接飘
SELECT total_amount INTO v_amount FROM orders WHERE id = p_order_id;
UPDATE orders SET pay_amount = v_amount * v_discount WHERE id = p_order_id;
END;
/
加个兜底就好了,没配折扣就当不打折,折扣率按 1.0 算:
sql
-- 修复:NVL 兜底,没折扣就当 1.0(不打折)
SELECT NVL(discount_rate, 1.0) INTO v_discount
FROM product_rule WHERE product_id = (SELECT product_id FROM orders WHERE id = p_order_id);
这种 bug,不调试光看代码真的很难发现--逻辑流程是对的,错的是数据。NULL 这玩意儿在关系库里就是个坑,参与运算默默给你搞出个空,还不报错。KStudio 那个单步调试,在这儿救了我半条命。
关键优化:大结果集的 fetchsize
接入跑通之后,又冒出一个性能问题。有个导出接口,查几十万行数据,跑着跑着内存就飙上去了,时不时 OOM。
查下来是 fetchsize 没设。默认情况下 JDBC 驱动会一次性把结果集全捞到客户端内存里,数据量一大直接撑爆。设上 setFetchSize,让驱动按需分批拉,内存立马就稳了。
另外金仓有个 enable_autocommit_fetch 参数,在自动提交模式下控制是全量返回还是按 fetchsize 按需返回。大结果集场景值得留意一下,默认行为不一定符合你预期:
java
// 大结果集查询:务必设 fetchsize,别让驱动一把全捞进内存
public void exportOrders(OutputStream out) throws SQLException, IOException {
String sql = "SELECT id, order_no, amount, create_time FROM orders WHERE create_time >= ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) {
// 自动提交 + 按需获取,配合 fetchsize 省内存
conn.setAutoCommit(true);
ps.setFetchSize(500); // 每次只拉 500 行
ps.setTimestamp(1, Timestamp.valueOf("2025-01-01 00:00:00"));
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
writeRow(out, rs); // 流式写出,内存占用恒定
}
}
}
}
落地效果
踩完这些坑,应用接入稳定运行,预发压测也过了。前后对比大致这样:
| 指标 | 接入初期(踩坑中) | 调优后 |
|---|---|---|
| 启动成功率 | 偶发类加载失败 | 100% |
| 连接池稳定性 | 间歇性"无法获取连接" | 连续压测无泄漏 |
| 大结果集导出内存峰值 | OOM 频发 | 稳定在 300MB 以内 |
| 存储过程排查耗时 | 肉眼盯代码,半天起步 | KStudio 单步,分钟级定位 |
写在最后
这趟接入干下来,我最大的感受是:换数据库这事,难的不是"能连上",而是"连得稳、跑得对"。
驱动版本、连接池验证语句、方言 SQL、存储过程空值,这几个坑单拎出来都不大,但凑一块儿够你忙活好几天。而且它们有个共同特点--本地测试的时候基本不冒头,非得上预发、压一压、跑跑真实数据才现形。这也是我为什么特别强调要尽早把应用丢到预发环境上去跑,别在本地自嗨。
几条心得,掏心窝子说一下。驱动 jar 一定对照 JDK 版本,别手滑,这步错了后面全白搭。连接池的 validationQuery 别照搬,得是目标库认的语句,配合 testWhileIdle 才管用。MyBatis 里的方言写法,趁接入的机会能清就清,别留给以后当定时炸弹。存储过程有 bug 别干瞪眼,KStudio 单步调试比肉眼强一百倍,变量值一目了然。大结果集别忘了 fetchsize,这个最容易忽略,也最容易 OOM。
金仓数据库在 JDBC、MyBatis 这些主流开发框架上的适配做得挺到位,驱动、连接、ORM 基本都能平滑接上,KStudio 的调试功能也确实好用,省了我不少排查时间。但适配归适配,该抠的细节一个都不能少。开发接入这活儿,慢点没关系,把每个环节都踩实了,后面上线才踏实。