前段时间在一台 Linux 服务器上做 DM8 单机实例部署顺手把表空间、用户、权限、基础表这些对象创建流程都走了一遍最后用匿名块批量造数时撞上了一个数组越界异常。整个过程里让我花时间最多的地方反而不是官方文档写得很全的部署步骤而是disable_jit这个参数和 JAVA 内嵌函数之间的关系以及 DM 管理工具自身的 JAVA 运行环境问题。这些内容分散在文档的不同章节里不实际跑一遍很难把它们串起来。这篇就当一份实操问题记录写给正在做 DM8 初始化部署、准备用 DM 管理工具建基础对象或者想了解匿名块异常处理写法的同行。我尽量把排查路径、SQL 示例和当时踩坑的原因都交代清楚方便你直接照着复现或避开。1. 单机实例部署中官方文档容易略过的 disable_jit1.1 先交代部署环境和基本流程我这次的部署环境是 Linux x86_648C16G 的虚拟机DM8 开发版系统账号用的dmdba。整体流程和官方《DM8 单机部署》文档基本一致解压安装包、用dminit初始化实例、注册系统服务、启动实例、最后用disql验证连接。初始化实例时我用的是下面这组参数./dminit PATH/dm/data PAGE_SIZE8 EXTENT_SIZE16 CASE_SENSITIVEY CHARSET1 PORT_NUM5236这里提醒一下PAGE_SIZE、CASE_SENSITIVE、CHARSET这几个参数一旦初始化完成后面基本改不动或者要付出很大代价去改。CASE_SENSITIVEY表示数据库对象名区分大小写CHARSET1表示 UTF-8 字符集。很多从 Oracle 迁过来的同事习惯性认为表名不区分大小写恰恰会在这一步埋下隐患。所以部署阶段就确认好这些参数比事后查半天文档要省事得多。服务注册和启动就不多说了按文档执行DmServiceDMSERVER相关的注册脚本即可。启动后先用最简单的方式确认实例状态ps -ef | grep dmserver ss -lnt | grep 5236这两条命令能快速确认进程是否存活、端口是否在监听。接着用disql SYSDBA/SYSDBAlocalhost:5236登录能顺利进入 SQL 提示符说明单机实例的基本骨架已经搭起来了。1.2 disable_jit 是怎么和 JAVA 内嵌函数扯上关系的部署完成后我开始做功能验证想建一个测试用的存储过程结果在 disql 里执行到涉及 JAVA 静态方法调用的语句时直接报错提示内容大概是“JAVA 环境不可用”或“内嵌 JAVA 函数执行失败”。我第一反应是系统里 JDK 没装好可这台服务器明明已经装了 JDK并且java -version完全正常。后来对照官方文档逐项排查才发现问题出在dm.ini里的disable_jit参数。文档里对它的描述很简短大致意思是“是否禁用即时编译启用 JIT 后可提升部分复杂表达式的执行效率”。它被归在系统参数那一类部署章节里基本不会提到。但实际环境下JIT 开启后会导致服务端内嵌 JAVA 函数的执行路径出问题具体表现就是 SQL 里调用 JAVA 内嵌函数时功能不可用或直接异常。处理方式很直接把这个参数改为 1也就是禁用 JITALTER SYSTEM SET disable_jit1;需要注意不同版本对参数是否动态生效的定义不太一样稳妥做法是在dm.ini配置文件中直接修改该参数然后重启实例。我这边就是改完配置文件后重启再执行之前失败的 SQL功能就恢复正常了。按官方文档的说法这个参数的本意是性能优化但它在某些版本下的实际影响已经超出了性能范畴直接关系到 JAVA 内嵌函数的可用性。这也是我为什么单独把这个问题拎出来讲文档把参数归类在“性能”里但你在功能验证阶段就可能会碰到它。1.3 部署完成后立刻验证的三件事经过这次教训我把部署完成后的验证清单固定成了三件事每次装完 DM8 都会先跑一遍用disql正常登录并执行SELECT * FROM V$VERSION;确认版本符合预期。调用一个简单内置包确认 JAVA 相关链路正常比如SELECT DBMS_RANDOM.VALUE FROM DUAL;。如果这一步抛出异常优先回头看disable_jit。检查dm.ini里的CASE_SENSITIVE、CHARSET、disable_jit是否和初始化设计一致。这三件事看着简单但都能在五分钟内帮你定位部署阶段最典型的几类问题实例没起来、参数配置不对、JAVA 链路异常。2. DM 管理工具连接与 JAVA 运行环境的连带问题2.1 管理工具连不上实例时的排查顺序实例部署好了接下来要用 DM 管理工具做图形化操作。我第一次连的时候也踩了坑工具一直提示连接失败。很多人的第一反应是查网络、查防火墙但实际上对于本机或同网段访问网络通常不是首要嫌疑。我的排查顺序是这样的先确认实例服务和端口没问题也就是前面提到的ps和ss两条命令再确认账号口令是否正确然后看数据库服务端字符集和客户端字符集是否匹配最后才看网络和防火墙。DM 管理工具的连接配置其实很简单主机名、端口、用户名、口令端口默认 5236。如果连接时报“网络通信失败”之外的其他错误比如登录被拒绝、口令错误那多半是账号权限或密码策略问题这类错误信息已经写得很明确了照着改就行。真正容易忽略的是服务端和客户端的字符集不一致导致的乱码或连接异常这个在创建实例时就应该提前想好。2.2 管理工具自身也是 JAVA 应用资源不足会连坐这里要特别说一个容易和第一节问题混淆的地方DM 管理工具是 JAVA 写的图形客户端它自身需要一套可用的 JAVA 运行环境。如果服务器或本机的默认 JDK 版本比较乱工具可能启动不了、界面空白甚至连上后操作一会儿就卡死。我当初把管理工具启动不了的问题误判成数据库实例问题排查了半天。后来发现是系统默认的 JAVA 版本和管理工具自带的 JRE 不一致导致。处理方式有两种一是直接用安装包自带的 JRE二是在管理工具启动脚本里显式指定 JAVA 路径。如果你的管理工具在操作大对象或跑复杂 SQL 时卡顿也可以适当调大启动脚本里的-Xmx参数给 JVM 分配更多内存。这里和服务器端disable_jit参数是两条独立的 JAVA 链路服务器端管的是数据库进程内嵌 JAVA 虚拟机管理工具管的是客户端进程自己的 JVM排查时不要混在一起。2.3 我的习惯管理工具建对象disql 跑脚本在实际操作中我通常把 DM 管理工具和 disql 分开用管理工具适合单对象创建、查看表结构、图形化授权、查看执行计划disql 适合跑批量脚本、反复执行匿名块、验证异常处理逻辑。原因很简单管理工具虽然方便但在处理大量脚本时复制粘贴和错误定位都不如命令行顺手。特别是匿名块这类带异常处理的过程性代码在 disql 里跑能看到更完整的错误输出出问题时也更容易复现。所以我接下来的基础对象创建虽然都在管理工具里操作但关键 SQL 我都是先在文本编辑器里写好后再粘贴到工具或 disql 里执行。3. 基础对象创建的标准顺序表空间先行、用户权限随后3.1 为什么先建表空间而不是先建用户达梦里创建用户时需要指定默认表空间所以建表空间必须先于用户创建。这个顺序如果搞反了后面再调整用户默认表空间会麻烦很多。我创建了一个独立表空间MYTBS专门给业务用户zm使用CREATE TABLESPACE MYTBS DATAFILE /dm/data/DMSERVER/MYTBS01.DBF SIZE 512 AUTOEXTEND ON NEXT 64 MAXSIZE 2048;这里SIZE 512表示初始大小 512MBAUTOEXTEND ON NEXT 64表示每次自动扩展 64MBMAXSIZE 2048表示上限 2048MB。如果你不建独立表空间业务对象会默认落在 MAIN 表空间和系统对象混在一起。一旦后续要迁移数据、清理表空间就会变得非常被动。表空间建好之后建议用管理工具左侧树形菜单刷新一下确认MYTBS状态正常。如果数据文件路径不存在创建会直接报错这种情况优先检查目录权限确保dmdba用户对目标目录有写权限。3.2 创建业务用户 zm 并做最小授权表空间就绪后创建用户CREATE USER ZM IDENTIFIED BY ZhongWenPwd2024 DEFAULT TABLESPACE MYTBS;这里有一个细节DM 在CASE_SENSITIVEY的情况下用户名和口令的大小写都是敏感的。如果你把口令用双引号包起来口令会严格按照大小写存储如果不加双引号口令会被统一转为大写。很多连接失败的问题就是口令大小写没有对上。创建用户之后做权限授予我的原则是最小授权绝不直接给 DBA 角色。这次我只是用zm做基础对象创建和造数测试所以给了以下权限GRANT RESOURCE TO ZM; GRANT CREATE TABLE TO ZM; GRANT CREATE VIEW TO ZM; GRANT CREATE PROCEDURE TO ZM; GRANT CREATE SEQUENCE TO ZM;RESOURCE角色在达梦里已经包含了建表、建索引等基础权限额外再显式加CREATE VIEW、CREATE PROCEDURE是保证后续测试时不会因为缺权限中断。生产环境建议在业务用户上只保留刚够用的权限能不动RESOURCE就不要动因为它的边界常常比你想象的大。3.3 建一张用于后续验证的基础表 ZS_RESULT_TEST有了用户和权限接下来创建测试表。我建的是ZM.ZS_RESULT_TEST专门用于后面的批量造数和异常处理验证CREATE TABLE ZM.ZS_RESULT_TEST( ID INT PRIMARY KEY, COL_NAME VARCHAR(100), COL_SCORE NUMBER(6,2), CREATE_TIME TIMESTAMP DEFAULT SYSDATE );字段设计很简单ID 主键COL_NAME 存随机字符串COL_SCORE 存分数CREATE_TIME 存插入时间。这里我特意让 CREATE_TIME 有默认值方便在造数时少写一个字段。建表后记得查一下表的归属和表空间SELECT OWNER, TABLE_NAME, TABLESPACE_NAME FROM USER_TABLES WHERE TABLE_NAME ZS_RESULT_TEST;如果当时连接的用户不是 ZM看到的可能就是空结果。这个不需要惊讶是权限视角的问题。表默认会创建在用户默认表空间 MYTBS 上如果你希望表和索引分开存放可以在建表语句里加上TABLESPACE MYTBS和INDEX TABLESPACE MYTBS_IDX之类的指定生产环境通常会把表和索引放在不同物理文件或者不同磁盘上以降低 IO 竞争。到这里基础对象创建的流程就闭环了表空间 - 用户 - 权限 - 表。每一步都依赖前一步的结果顺序错了就会遇到“权限不足”或“默认表空间不存在”这类问题。4. For 循环与关联数组批量造数Good 案例复盘4.1 造数场景与常见写法的差异基础表建好后我需要往里插入大量测试数据大概十万行左右。字段要求带随机字符串、随机分数和时间戳用来做后续查询性能验证。很多人第一反应是写一个简单的循环逐行 INSERT比如FOR I IN 1..100000 LOOP INSERT ...; END LOOP;。但如果你在循环里不做任何提交最后统一 COMMIT这种方式本身没问题问题在于它不够灵活而且边界条件写错时连异常都来不及处理。我当时用的是 FOR 循环加关联数组的写法先把主键 ID 放进一个数组里再遍历数组执行插入DECLARE TYPE T_ID_LIST IS TABLE OF INT INDEX BY INT; V_IDS T_ID_LIST; V_TOTAL INT : 100000; BEGIN FOR I IN 1..V_TOTAL LOOP V_IDS(I) : I; END LOOP; FOR IND IN 1..V_IDS.COUNT LOOP INSERT INTO ZM.ZS_RESULT_TEST(ID, COL_NAME, COL_SCORE) VALUES(V_IDS(IND), DBMS_RANDOM.STRING(U, 10), ROUND(DBMS_RANDOM.VALUE(60, 100), 2)); END LOOP; COMMIT; END; /这段代码里DBMS_RANDOM.STRING(U, 10)生成 10 位大写随机字符串DBMS_RANDOM.VALUE(60, 100)生成 60 到 100 之间的随机数外层再用ROUND(..., 2)保留两位小数。4.2 为什么这是 Good 案例这个写法好在三个地方。第一循环上界用的是V_IDS.COUNT不是硬编码的 100000。数组元素是前面一个循环动态填充的这样即使总数变了遍历逻辑也不需要改。第二先把所有 ID 准备好再统一插入数据源可控适合后面追加其他随机字段。第三整个匿名块内的事务边界很清晰要么全部提交要么全部回滚不会出现插了一半数据再手工清理的尴尬局面。当然如果你只是为了快速造数不管字段内容多复杂SQL 层面还有更快的方法比如INSERT INTO ZM.ZS_RESULT_TEST(ID, COL_NAME, COL_SCORE) SELECT LEVEL, DBMS_RANDOM.STRING(U, 10), ROUND(DBMS_RANDOM.VALUE(60, 100), 2) FROM DUAL CONNECT BY LEVEL 100000;这种写法比 FOR 循环要快不少因为它在一次 SQL 语句里完成所有数据生成。但它的短板也明显一旦你要对每一条数据做不同的逻辑处理或者根据外部变量决定是否插入纯 SQL 就力不从心了。FOR 循环加关联数组的定位是“可编程的批量造数”适合那些数据生成规则复杂的场景。4.3 这个写法最常见的隐患循环边界越界回到 FOR 循环写法的隐患最典型的就是数组下标越界。达梦的关联数组下标默认从 1 开始这一点和 Oracle 一致。如果你下意识写了FOR IND IN 0..V_IDS.COUNT第一次循环就会访问V_IDS(0)而数组里根本没有这个下标的元素直接触发数组上界越界异常。还有一种更隐蔽的情况如果V_IDS是空数组V_IDS.COUNT等于 0此时FOR IND IN 1..0这种递减区间在达梦里同样是非法操作。这些边界问题在没有异常处理块包裹时整个匿名块会直接中断之前已经执行成功的 INSERT 全部回滚。这也是为什么我每次写匿名块之前都会先提醒自己循环边界和异常处理至少要有一个是靠谱的。最稳的做法是两个都做也就是接下来要讲的内容。5. 越界异常 NUO_ARRAY_EXCEPTION 的完整排查与异常处理块改造5.1 先把错误现场完整捞出来为了验证越界问题我故意写了一个从 0 开始的循环DECLARE TYPE T_ID_LIST IS TABLE OF INT INDEX BY INT; V_IDS T_ID_LIST; V_IND INT; BEGIN FOR I IN 1..100 LOOP V_IDS(I) : I; END LOOP; FOR V_IND IN 0..V_IDS.COUNT LOOP NULL; END LOOP; END; /执行后disql 给出的报错信息包含两个关键内容错误码和错误描述。我这边看到的是数组上界越界 NUO_ARRAY_EXCEPTION对应的错误码为-4029。这个环节最重要的是先确认错误码因为后面的异常处理块要根据错误码来做精确捕获。定位时我先把循环体改成NULL最小化出错范围确认问题不在 INSERT 语句本身而是循环到达V_IDS(0)时才触发。接着把起点改成 1异常立刻消失。这就能确认是下标越界而不是表结构或权限问题。5.2 用 DECLARE...BEGIN...EXCEPTION...END 改造匿名块达梦的 PLSQL 匿名块和 Oracle 类似整体结构是DECLARE 变量声明、异常声明 BEGIN 业务逻辑 EXCEPTION 异常处理逻辑 END;或DECLARE ... BEGIN ... EXCEPTION ... END; /EXCEPTION段就是专门承接异常处理的 SECTION。异常发生时控制权会跳到这一段。我先用最通用的WHEN OTHERS把错误捞出来写进日志表先建一张错误日志表CREATE TABLE ZM.ERR_LOG( ID BIGINT IDENTITY(1,1) PRIMARY KEY, ERR_CODE INT, ERR_MSG VARCHAR(500), OCCUR_TIME TIMESTAMP );然后是带异常处理的匿名块DECLARE TYPE T_ID_LIST IS TABLE OF INT INDEX BY INT; V_IDS T_ID_LIST; V_IND INT; V_ERR_CODE INT; V_ERR_MSG VARCHAR(500); BEGIN FOR I IN 1..10 LOOP V_IDS(I) : I; END LOOP; FOR V_IND IN 0..V_IDS.COUNT LOOP INSERT INTO ZM.ZS_RESULT_TEST(ID, COL_NAME) VALUES(V_IDS(V_IND), TEST_ || V_IND); END LOOP; COMMIT; EXCEPTION WHEN OTHERS THEN V_ERR_CODE : SQLCODE; V_ERR_MSG : SQLERRM; INSERT INTO ZM.ERR_LOG(ERR_CODE, ERR_MSG, OCCUR_TIME) VALUES(V_ERR_CODE, V_ERR_MSG, SYSDATE); ROLLBACK; END; /执行后虽然 INSERT 没有成功但ERR_LOG表里会多一条记录把-4029这个错误码和对应的错误消息完整记录下来。这个习惯非常重要尤其是在匿名块比较长、业务逻辑复杂时没有日志几乎等于瞎猜。5.3 PRAGMA EXCEPTION_INIT 绑定具名异常如果你想针对某个特定错误码做不同的处理可以在声明段用PRAGMA EXCEPTION_INIT把错误码绑定到一个具名异常上DECLARE NUO_ARRAY_EXCEPTION EXCEPTION; PRAGMA EXCEPTION_INIT(NUO_ARRAY_EXCEPTION, -4029); TYPE T_ID_LIST IS TABLE OF INT INDEX BY INT; V_IDS T_ID_LIST; V_IND INT; BEGIN FOR I IN 1..10 LOOP V_IDS(I) : I; END LOOP; FOR V_IND IN 0..V_IDS.COUNT LOOP INSERT INTO ZM.ZS_RESULT_TEST(ID, COL_NAME) VALUES(V_IDS(V_IND), TEST_ || V_IND); END LOOP; COMMIT; EXCEPTION WHEN NUO_ARRAY_EXCEPTION THEN INSERT INTO ZM.ERR_LOG(ERR_CODE, ERR_MSG, OCCUR_TIME) VALUES(-4029, 数组下标越界请检查循环起点和边界, SYSDATE); ROLLBACK; WHEN OTHERS THEN INSERT INTO ZM.ERR_LOG(ERR_CODE, ERR_MSG, OCCUR_TIME) VALUES(SQLCODE, SQLERRM, SYSDATE); ROLLBACK; END; /这里PRAGMA EXCEPTION_INIT(NUO_ARRAY_EXCEPTION, -4029)的含义是把错误码-4029绑定到自定义异常名NUO_ARRAY_EXCEPTION上。之后在EXCEPTION段就可以用WHEN NUO_ARRAY_EXCEPTION精确捕获这个错误而不会被WHEN OTHERS抢先接走。要注意PRAGMA EXCEPTION_INIT必须放在声明段并且要在变量声明之后、BEGIN之前。绑定时错误码要和实际报错一致不同小版本下错误码可能有差异保险做法是先用WHEN OTHERS配合SQLCODE查一次实际错误码再决定绑定值。如果你不想用具名异常也可以直接在WHEN OTHERS里判断SQLCODE -4029后做分支处理。两种方式效果类似区别在于具名异常在代码可读性上更好适合同一段逻辑里对不同错误做差异化响应的场景。5.4 实际处理策略继续执行还是整体回滚最后一个关键问题异常发生后业务到底是继续还是中断。这取决于场景。如果是一次性造数任务推荐在EXCEPTION段记录日志后整体回滚修正代码再跑。因为测试数据要求完整性部分插入会导致后续统计结果失真。如果你在做批量数据加工希望跳过坏数据处理后续数据就不要在顶层用大而全的异常块而是把异常处理下沉到循环内部。在循环体里再套一个内层 BEGIN...EXCEPTION 块FOR V_IND IN 1..V_COUNT LOOP BEGIN INSERT INTO ZM.ZS_RESULT_TEST(ID, COL_NAME) VALUES(V_IDS(V_IND), TEST_ || V_IND); EXCEPTION WHEN OTHERS THEN INSERT INTO ZM.ERR_LOG(ERR_CODE, ERR_MSG, OCCUR_TIME) VALUES(SQLCODE, SQLERRM, SYSDATE); CONTINUE; END; END LOOP;内层捕获异常后CONTINUE循环会继续跑但外层不会感知到这条失败记录。如果你希望内层失败只记录、最终整体仍然回滚可以在内层异常处理中不做提交并在循环结束后判断日志表是否有新增记录再决定是否 ROLLBACK。这算是我实际项目中用得比较多的处理思路逻辑清晰也方便事后核对失败数据。状态比较理想的结构是一个可重复执行的匿名块内层负责单条数据的失败跳过和数据记录外层负责事务边界和最终提交决策。这样无论造数还是批量加工都能在出问题时快速定位到具体数据而不是整体黑屏。最后分享一个实际体会这一轮操作下来我最大的收获是把部署检查清单从“能启动就算成功”改成了“启动后必须验证关键功能链路”。disable_jit这种藏在性能参数里的开关不实际执行 JAVA 内嵌函数调用根本发现不了问题匿名块也尽量从一开始就包好异常处理日志表提前建好不要等报错后再补。造数这种看起来简单的需求往往就是循环边界和异常处理这种小地方最容易翻车。
阅读完成 · 觉得有帮助?