瀚高数据库抽取工具:Oracle存量数据迁移至HGDB的实战指南 简介瀚高数据库抽取工具是一套面向Oracle与瀚高数据库的数据迁移及同步方案覆盖数据库升级、系统替换、双库并行等运维场景。该工具以抽取-转换-加载为核心内置对Oracle存储过程、触发器、索引等特性的兼容处理可帮助DBA与数据运维人员降低手工迁移成本。资源为zip压缩包共660个文件约55.29MB主体为jar驱动、dll动态库、exe启动程序、properties配置等并携带大量时区规则文件与配置模板便于处理跨地域时间数据并快速调整运行参数。工具对Oracle 10g等版本有专门优化运行依赖JRE7使用前需准备对应Java环境结合描述中的数据同步、异常恢复要点用户可获得运行组件、配置参考和迁移思路。目前已有775人浏览学习适合国产化数据库迁移项目的初期评估、环境准备与实操入门。1. 瀚高数据库抽取工具把 Oracle 存量搬到 HGDB 的轻量搬运动作把一套跑了多年的 Oracle 库迁进瀚高数据库HGDB最让人心里没底的往往不是业务改造而是那几十张表、几百万行存量数据怎么安全落地。手写脚本改一个表半天重同步工具又贵又沉这个瀚高数据库抽取工具就是奔着这个场景来的——改一个 yaml 任务文件命令行跑一轮就能把指定业务域的表结构、数据、序列一块儿搬到 HGDB 对应库里。它承担的是整个迁移链路里最枯燥的一段读源库元数据生成目标建表语句按配置批量拉数最后把序列重置到正确位置。适合正在做 Oracle 下线、国产数据库选型替换的 DBA 和业务系统负责人也适合刚接触 HGDB、想拿一份真实数据练手的后端同学。下文把配置、参数、代码逻辑和踩过的坑一次讲完。2. 整体结构与最小跑通三个配置文件把你从零带到一百万行拿到工具先别急着点运行花十分钟理解它的文件布局和配置项后面能少走很多弯路。这一章讲清楚工具在迁移流程里的位置、每个配置项干了什么以及怎么用最小配置跑通一张表的全量抽取。2.1 抽取工具在整个迁移流程里的位置一次完整的 Oracle 到 HGDB 迁移通常拆成五步结构评估、结构迁移、数据抽取、数据校验、应用切换。这个工具负责的是第三和第四步的一部分——它不做全库评估也不负责应用侧改造而是把「按表搬数」这件事做到极致。工具本身是纯 JDBC 命令行程序没有中间件不依赖额外服务。它直连 Oracle 源库和 HGDB 目标库启动后按任务文件逐表处理。这种设计的好处很直接迁移窗口不用等调度系统服务器上扔一个目录就能跑坏处也很明显——源库连接串、目标库连接串、业务表清单都得自己维护清楚配置错了它会老老实实报错不会帮你自动纠偏。我一般建议把工具放在迁移专用机器上和业务系统网络隔离开。源库只开迁移账号给 SELECT 权限目标库给建表、写数、建序列的权限。别图省事用业务账号直连出问题的时候排查边界会非常困难。2.2 目录结构与配置文件说明解压之后目录长这样extractor/ ├── config/ │ ├── application.yml │ └── tasks/ │ └── member_order.yaml ├── sql/ ├── logs/ └── bin/ └── start.shapplication.yml管两端连接和一个任务的默认行为tasks下每个 yaml 对应一张表或一组表的抽取规则。核心配置如下source: jdbcUrl: jdbc:oracle:thin://10.0.0.5:1521/ORCL user: MIG_USER password: Encrypted123 schema: MIG fetchSize: 5000 target: jdbcUrl: jdbc:hgdb://10.0.1.8:5866/hgdb user: hgdb_user password: Mig123 schema: public rewriteBatchedStatements: true task: seqReset: true dropExistingTable: true chunkSize: 500重点说三个容易被忽略的fetchSize: 5000控制 Oracle 端游标每次从数据库取多少行默认 10 的话大表会慢到你想砸机器rewriteBatchedStatements: true是让 HGDB 端 JDBC 把一批 insert 重写成一条多值 insert批量性能能差出好几倍seqReset: true决定抽取完成后是否自动把目标库序列重置到源库当前值。任务文件更简单它决定抽哪张表、按什么条件抽taskName: member_order sourceTable: MEMBER_ORDER targetTable: member_order columns: * whereClause: UPDATE_DATE sysdate - 30 orderBy: ORDER_ID partitionColumn: ORDER_IDwhereClause是最常用的增量条件抽全量就删掉这行partitionColumn配合chunkSize做分段抽取大表必须分段否则一个长事务把源库回滚段拖爆运维会来找你喝茶。2.3 单表全量抽取的最小可用案例拿一张 100 万行的订单表练手。先准备一个干净的任务文件member_order.yaml去掉whereClause保留orderBy和partitionColumn然后执行cd /opt/extractor bin/start.sh --task config/tasks/member_order.yaml工具启动后会在logs/下生成当天日志正常情况下能看到每个阶段的耗时统计元数据读取、DDL 生成、数据抽取、序列重置。跑完去目标库确认行数select count(*) from public.member_order;源库同样查一条两边数字对上就完成最小闭环。这个案例虽然简单但把工具的三个核心动作全走了一遍——读结构、搬数据、重置序列。后面章节讲的类型映射、批量写入、踩坑记录全是围绕这三个动作展开的。3. 连接参数与类型映射在这里翻车的人比想象中多配置能跑通只是第一步真正决定抽取工具能不能用于生产的是连接细节和类型映射。这一章把 JDBC 参数、Oracle 到 HGDB 的默认类型对应关系以及最容易静默出错的三类字段问题讲透。3.1 JDBC 连接串参数fetchSize 与批量写Oracle 侧连接串建议这样写jdbc:oracle:thin://10.0.0.5:1521/ORCL不用 SID 方式用服务名方式迁移环境里 service_name 比 SID 稳定得多。连接建立后第一件事就是给 Statement 设置两个关键参数Connection conn DriverManager.getConnection(url, user, password); conn.setAutoCommit(false); PreparedStatement ps conn.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); ps.setFetchSize(5000);TYPE_FORWARD_ONLY和CONCUR_READ_ONLY是流式读取的前提。Oracle 驱动默认把结果集全部拉回客户端内存大表直接 OOM设置 fetchSize 之后驱动按指定行数分批从数据库取内存占用恒定这是大表抽取不翻车的第一道保险。HGDB 目标侧连接参数里务必带上批量重写jdbc:hgdb://10.0.1.8:5866/hgdb?rewriteBatchedStatementstrue没这个参数时JDBC 的addBatch是一条一条发给数据库网络往返开销大得吓人。开了之后驱动把多条 insert 合并成insert into ... values (...),(...),(...)形式实测批量插入吞吐能提升三到五倍。抽取工具代码里也建议在任务配置层暴露这个开关不同环境网络状况不一样统一写死反而麻烦。3.2 默认类型映射表Oracle 到 HGDB 的十六个对应关系类型映射是整个抽取工具里最不能拍脑袋的部分。Oracle 有 NUMBER、VARCHAR2、DATE 这些独有类型HGDB 基于 PostgreSQL 体系两边语义不同硬搬必然出问题。工具默认带一张映射表可以按业务覆盖Oracle 类型HGDB 映射类型说明NUMBER(p,0)p5SMALLINT小整数省空间NUMBER(p,0)p9INTEGER常用整数NUMBER(p,0)p18BIGINT大整数NUMBER(p,s)NUMERIC(p,s)小数原样保留精度VARCHAR2(n)VARCHAR(n)长度不变NVARCHAR2(n)VARCHAR(n)统一按 UTF-8 处理CHAR(n)CHAR(n)定长带空格DATETIMESTAMPOracle DATE 含时分秒TIMESTAMP(p)TIMESTAMP(p)精度保留CLOBTEXT大文本BLOBBYTEA二进制RAW(n)BYTEA二进制FLOATDOUBLE PRECISION浮点BINARY_FLOATREAL单精度浮点映射规则不是死的。比如NUMBER不带精度时Oracle 里可以存任意精度小数工具默认映射成NUMERIC但如果你确认业务字段是整数可以配置覆盖成BIGINT查询性能会更好。反过来如果源表里有NUMBER(38)这种极限精度映射到NUMERIC(38)后写入效率会低这种情况建议先跟业务确认是否能降精度。3.3 空字符串、NULL、精度三个最容易静默出错的字段先说空字符串和 NULL。Oracle 里会被当成 NULL 存进去而 HGDB 的 VARCHAR 字段里空字符串和 NULL 是两个完全不同的值。抽取工具如果直接按 JDBCgetString()读读出来的 Oracle 空串就是 null写进 HGDB 也变成 NULL。表面看数据没丢但业务侧查询条件where col 会查不出原本该命中的行。解决办法是在抽取前做一次源库扫描找出疑似空串的字段按业务确认后决定统一转 NULL 还是保留空串。工具里可以加一个开关对 VARCHAR 类字段把null转成空串写入。再说精度。Oracle 的NUMBER类型允许定义小数位数但有些老表建的时候没带精度抽取工具读元数据时拿到的DATA_PRECISION是空值映射就会出偏差。遇到这种情况我一般是单独查 Oracle 数据字典确认实际取值范围再在任务配置里手动指定映射类型。最后是字符集。源库是 ZHS16GBK目标库是 UTF-8 时VARCHAR2 的字节长度和字符长度会不一致。工具读COLUMN_SIZE时按字节长度返回直接搬过去建表字段长度会比实际需要大一倍或产生溢出。处理方式是在元数据读取阶段把字节长度除以字符集最大字节数换算成字符长度再生成目标 DDL。4. 核心抽取代码拆解从表 DDL 翻译到数据批量落库原理说完了这一章直接进代码。工具的核心逻辑分三段读源库元数据生成 HGDB 建表语句、流式读数据批量写入、重建序列。每段都有值得细看的参数和边界处理。4.1 元数据读取和 DDL 翻译元数据读取用的是 JDBC 标准DatabaseMetaData但 Oracle 的实现有不少特别之处。核心代码如下DatabaseMetaData meta sourceConn.getMetaData(); ResultSet cols meta.getColumns(null, schema, tableName.toUpperCase(), %); while (cols.next()) { String colName cols.getString(COLUMN_NAME); String typeName cols.getString(TYPE_NAME); int byteLength cols.getInt(COLUMN_SIZE); int decimalDigits cols.getInt(DECIMAL_DIGITS); int nullable cols.getInt(NULLABLE); // Oracle 的 COLUMN_SIZE 是字节长度按字符集换算成字符长度 int charLength byteLength / maxBytesPerChar; String targetType mapOracleToHgdb(typeName, charLength, decimalDigits); String nullFlag (nullable DatabaseMetaData.columnNullable) ? : NOT NULL; ddlBuilder.append(\).append(colName.toLowerCase()).append(\ ) .append(targetType).append( ).append(nullFlag).append(,); }这段代码有两个必须处理的坑。第一个是 Oracle 的COLUMN_SIZE返回的是字节长度如果源库是 ZHS16GBK 且字段是 VARCHAR2(20)这里拿到的值可能是 40直接建到 HGDB 会多出一倍容量除字符集最大字节数是常见做法。第二个是列名大小写Oracle 默认大写HGDB 建表时如果不加双引号会自动转小写显式转小写并加双引号能让后续查询省掉很多大小写踩坑。mapOracleToHgdb就是第 3 章那张类型映射表的代码实现switch 里按TYPE_NAME分发精度边界在分发时判断。表结构生成完紧接着生成主键、索引和注释约束名要做去冲突处理Oracle 的约束名最长 30 字节HGDB 长度限制不同截断后要加哈希后缀防重名。4.2 数据抽取主循环流式读批量写数据抽取是工具的核心循环。这里给出一个简化但完整的版本String sql SELECT columns FROM sourceTable (whereClause.isEmpty() ? : WHERE whereClause) ORDER BY orderBy; try (PreparedStatement ps sourceConn.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) { ps.setFetchSize(5000); try (ResultSet rs ps.executeQuery(); PreparedStatement insert targetConn.prepareStatement(insertSql)) { int batchCount 0; String lastPartitionValue ; while (rs.next()) { int idx 1; for (ColumnMeta col : columnsMeta) { Object value rs.getObject(idx); // CLOB 等大对象需要特殊处理见下文 if (value instanceof java.sql.CLOB) { value rs.getString(idx); } insert.setObject(idx, value); } insert.addBatch(); batchCount; lastPartitionValue rs.getString(partitionColumn); if (batchCount % chunkSize 0) { insert.executeBatch(); targetConn.commit(); log.info(已抽取到分区键: {}, lastPartitionValue); batchCount 0; } } if (batchCount 0) { insert.executeBatch(); targetConn.commit(); } } }逻辑说明外层prepareStatement用 forward-only 和 read-only配合fetchSize(5000)形成流式读取内层每读一行addBatch攒够chunkSize就执行一次批量插入并提交。参数说明chunkSize在任务配置里定义500 是一个保守值批量太小网络往返多批量太大单个事务过长、出错回滚代价高。lastPartitionValue记到日志里如果中途失败崩溃可以按这个值写增量条件续抽不用从头开始。这里有个容易被忽略的细节rs.getString(partitionColumn)不能在addBatch之后调用否则游标状态变化可能导致取值错位要在进入下一行之前取。4.3 序列、自增与主键迁移完不处理等于埋雷数据搬完序列不处理业务侧一插入新数据就主键冲突。Oracle 里序列是独立对象通常配合触发器给表生成主键HGDB 里用的是序列加DEFAULT nextval(...)或serial。抽取工具在数据落库后会自动生成序列重置 SQLselect setval(member_order_id_seq, 19000001, true);19000001来自源库序列的当前值。读取方式很简单select sequence_name, last_number from user_sequences where sequence_name SEQ_MEMBER_ORDER;last_number是 Oracle 序列缓存的边界值不是严格意义上的当前值但对于迁移场景够用——重置后目标库新生成的主键一定大于源库已存在的最大值。稳妥的做法是先查目标表max(id)再查源库序列last_number两者取较大者作为setval参数。序列名也要处理。Oracle 序列名通常全大写长度 30 字节HGDB 序列放在publicschema 下名字建议统一按表名_列名_seq重命名避免后续运维混淆。工具里如果开了seqReset: true这些动作会自动完成没开的话就得在迁移清单里单列一步手动执行漏掉的后果基本是上线当晚业务插入报主键冲突。5. 踩坑与排查抽取过程中五个高频翻车现场再严谨的代码也挡不住真实数据里的幺蛾子。这一章写我实际跑抽取任务时遇到过的五个高频问题每条按现象、原因、解决三段式记录希望能帮你省几晚上排查时间。5.1 CLOB 字段内容截断现象某张表有个备注字段是 CLOB抽取完成后抽查数据发现内容只写到 4000 字符就断了个别行的中文变成乱码。原因两处。一是读取时直接用了getString()Oracle JDBC 驱动对超过缓冲区的 LOB 内容会按maxFieldSize截断二是写入时把 Oracle 的 CLOB 内部结构直接传给 HGDBPG 体系不认这种结构二进制内容被当文本处理出现乱码。解决读侧对java.sql.CLOB类型强制走rs.getString()前先free()旧 LOB或者直接用getCharacterStream()分段读完再拼写侧统一把 CLOB 转成 Java String 再setString。代码里就是 4.2 节注释里那句if (value instanceof java.sql.CLOB)的处理逻辑。还在代码里给 CLOB 字段加了长度校验抽取完比对源和目标字段长度超过 1% 偏差直接报错。5.2 大表抽取越来越慢现象小表秒级完成几百万行的主表跑半小时后速度断崖式下降日志里时间戳越拉越长最后 JVM 内存也飙高。原因有两个叠加问题。第一个是结果集被全部读入内存典型的 fetchSize 没生效——检查发现连接串里被人加了useFetchSizeWithLongColumnfalse之类的参数Oracle 对含 LOB 列的查询会忽略 fetchSize 设置第二个是 commit 频率太低chunkSize设置成 5000单事务写入量过大HGDB 端 WAL 刷盘和索引维护拖慢了整体速度。解决把大表的抽取 SQL 拆成多个分区段按主键范围循环执行每段 30 万行段间独立提交。同时在任务配置里把大表chunkSize调回 500牺牲一点吞吐换稳定性。跑完对比两边的插入速率日志基本能定位是哪个环节掉了链子。5.3 数据条数对得上内容差几行现象count(*)两边完全一致抽样比对主键也一致但业务侧反馈某几个客户的金额对不上。复查发现有一行的时间字段少了 8 小时还有几行小数精度变了。原因时间问题是 session 时区不一致。源库是东八区HGDB 目标库建库时用了 UTCTIMESTAMP WITHOUT TIME ZONE写入时按 session 时区解释两边转换自然差 8 小时。精度问题是目标字段映射成了NUMERIC但源库该列实际是BINARY_DOUBLE浮点转定点时小数点后的尾数被舍入。解决抽取前固定两端 session 时区Oracle 侧执行ALTER SESSION SET TIME_ZONE Asia/ShanghaiHGDB 侧执行SET TIME ZONE Asia/Shanghai。精度问题在确认字段实际类型后把该列的映射覆盖为DOUBLE PRECISION重新抽一遍。从那以后我把「时区对齐」写进了每个任务文件的固定配置不再依赖机器默认值。5.4 外键约束导致写入失败现象任务跑到第 17 张子表时报外键冲突批量插入中断目标库剩下半截数据。原因任务文件里表的排列顺序没有按依赖关系排主表还没抽完先抽了引用它的子表。HGDB 目标库建表时把外键约束一并建上了插子表数据时外键校验发现主表缺行直接拒绝写入。解决两步。第一步抽取顺序按外键依赖拓扑排序工具里生成任务清单时先查数据字典里的外键关系把被引用表排在前面。第二步生产环境迁移窗口紧张时直接在目标库先ALTER TABLE ... DISABLE TRIGGER ALL等全部数据落库、校验通过后统一启用约束再跑一遍外键检查。后者速度快但考验校验环节的完整性漏掉检查上线就出大事。5.5 时间字段差 8 小时现象5.3 里提到的时间问题单独拎出来因为它太容易隐蔽。所有 TIMESTAMP 字段都差 8 小时但日期字段正常——业务侧报障时第一反应往往是程序 bug。原因Oracle 的DATE类型本身就包含时分秒映射到 HGDB 的DATE后时分秒被截掉了只剩日期正常。而TIMESTAMP字段受时区影响整体偏移。两类字段混在一张表里就出现「部分正常部分偏移」的诡异现象。解决映射表里DATE - TIMESTAMP不让它落成 HGDB 的DATE。同时抽取代码里对 TIMESTAMP 类型统一取resultSet.getTimestamp()避免驱动返回本地时区的时间戳再被 JDBC 二次转换。这个坑在文档里写得很隐晦不实际踩一次很难注意。6. 收尾技巧抽取完成后十分钟内的校验清单数据抽完不是结束切库前校验这一步省不掉。我自己的习惯是三件事行数、边界值、序列。每次任务结束固定跑一遍十分钟内能挡住绝大部分低级问题。-- 1. 行数与主键边界 select count(*), min(id), max(id) from public.member_order; -- 2. 多列聚合哈希粗粒度对比源和目标内容 select count(*), sum(md5(order_no || | || coalesce(amount::text, null))) as content_hash from public.member_order; -- 3. 序列起点必须大于当前最大主键 select last_value from member_order_id_seq;第一句对比源库的同名查询行数、最小最大主键一眼能看出是否有丢段。第二句用 md5 拼接关键列做内容级校验两边哈希一致基本能确认内容没跑偏注意 group by 时要排除 NULL 拼接的坑coalesce 兜底是为了避免空值导致哈希不一致。第三句检查序列last_value小于max(id)就说明序列没重置上线必炸。校验脚本建议独立成bin/verify.sh参数传表名自动跑完三句 SQL 并把结果打印成对比表。工具本身不送你全套校验但这个脚本自己写也就半小时值得做。这个习惯帮我挡过至少两次事故——一次是某表单据量过大导致分段抽取漏了一段一次是序列重置脚本没跑导致上线后主键冲突。从那以后我每个抽取任务结束都强制走一遍三句 SQL 校验不通过不切库。希望帮到你。本文还有配套的精品资源点击获取