Nacos 适配达梦数据库:信创环境注册配置中心落地实践
简介:面向需要在国产化环境中将 Nacos 注册配置中心接入达梦数据库的开发与运维人员,这份资源提供已适配达梦的数据源版本,兼顾 Windows 与 Linux 运行环境,并保留可自行修改、重新编译的源码部分,方便按项目需求调整兼容逻辑。压缩包共 92 个文件、约 297.51MB,包含日志、Raft 元数据与状态文件、SQL 脚本、Shell 脚本、YAML/XML/Properties 配置、JAR 及多种压缩包等,既有可直接部署使用的编译产物,也有可导入 IDEA 的工程源码,达梦建库脚本可自行导入。目前已有 895 人学习下载。读者可据此快速搭建 Nacos 连接达梦的验证环境,查看适配后的配置与启动脚本,并基于源码继续排查兼容问题、调整连接参数或二次编译,适合作为国产数据库替代场景下的参考实现与改造起点,便于快速对照与落地验证。
1. 换上达梦之后,Nacos 才算真正能在信创环境里落地
接手一个要在信创环境里落地的注册中心,第一反应往往是把 Nacos 默认的内嵌 Derby 换掉。换 MySQL 好办,换达梦就完全是另一回事:驱动类名对不上、建表脚本跑不通、mapper 里的LIMIT a, b和ON DUPLICATE KEY UPDATE一条条报错,控制台能打开,但配置列表永远是空的。这套「nacos 适配达梦数据源」的改造资源,解决的就是从 Derby/MySQL 切到达梦的这一整段链路——驱动外挂、数据源配置、建表脚本转换、MyBatis 方言增量、分页与自增主键改写。改完之后,Nacos 能正常读写配置、注册实例、开启鉴权。适合正在做国产化数据库替换的后端和运维,也适合要基于 Nacos 做二次开发、需要把持久化层彻底摸清的人。
2. 摸清 Nacos 持久化链路:达梦到底要插在哪一层
动手改之前,得先搞清楚 Nacos 是从哪儿拿到DataSource的。很多人第一反应是去改 Spring 的spring.datasource.url,改完启动发现压根没生效,白折腾半天。原因是 Nacos 根本不吃 Spring Boot 的标准数据源配置,它自己实现了一套多数据源装配逻辑,入口在ExternalDataSourceServiceImpl这个类里。
2.1 内嵌 Derby 与外置库的分支判断
Nacos 启动时会读spring.datasource.platform这个属性。这个 key 名字长得很像 Spring 自己的东西,实际上是 Nacos 自定义的开关,别和 Spring Boot 的spring.sql.init.platform混为一谈——这两个搞混是新手最容易翻的车之一。
判断逻辑大致是这样:spring.datasource.platform为空或者值为derby时,走内嵌 Derby,数据落在${nacos.home}/data/derby-data下;一旦它被设成别的值(mysql、dm都行),并且配了db.num与db.url.0,就切到ExternalDataSourceServiceImpl,由 Nacos 自己按db.url.N、db.user.N、db.password.N这组带下标的键去拼数据源数组。
配置文件里长这样:
# 关掉内嵌 Derby,走外置数据源分支 spring.datasource.platform=dm # 数据源个数,达梦一般就一个实例,写 1 db.num=1 db.url.0=jdbc:dm://127.0.0.1:5236?schema=NACOS db.user.0=NACOS db.password.0=YourPassworddb.num是 Nacos 多数据源分摊的入口,它支持把不同的表哈希到不同的库上,MySQL 场景下有人拿它做分库。达梦这边我建议老老实实写 1,因为多数据源分摊要求每个库的表结构完全一致,还得自己保证一致性哈希不漂移,收益远小于维护成本。db.url.N里的schema=NACOS是达梦特有的,指定默认模式,省掉后面每条 SQL 都带 schema 前缀的麻烦。
这里有个细节:spring.datasource.platform的值并不参与 SQL 方言判断,它只是个开关。真正决定 SQL 怎么写的,是 MyBatis 的databaseId或者你手改的 mapper。所以填dm、dameng、mysql在功能上没区别,填dm只是为了后续接DatabaseIdProvider时语义对得上。
2.2 达梦接入的三个卡点
把这条链路捋直之后,卡点就非常明确了,一共三处。
第一处是驱动。达梦的 JDBC 驱动类是dm.jdbc.driver.DmDriver,jar 包通常是DmJdbcDriver18.jar(对应 JDK 8 及以上)。Nacos 的发行包里不带你这个 jar,得自己放进去。
第二处是建表脚本。Nacos 源码的distribution/conf下自带mysql-schema.sql和derby-schema.sql,没有达梦版本。MySQL 脚本里那些AUTO_INCREMENT、ENGINE=InnoDB DEFAULT CHARSET=utf8、反引号包字段名、行内COMMENT,达梦一条都不认。必须整份重写,不能指望“差不多能跑”。
第三处是mapper 里的方言。Nacos 的 MyBatis XML 是按 MySQL 写的,里面有LIMIT #{start}, #{pageSize}这种逗号分页,有INSERT ... ON DUPLICATE KEY UPDATE这种 MySQL 独有语法,也有now()、IFNULL这类函数。达梦在 MySQL 兼容模式下能认一部分,但你不能把生产环境押在“兼容模式大概能兜住”上——玄学一旦生效,报错信息往往指向一个完全不相干的地方。
2.3 1.x 与 2.x 的差异,别拿旧教程硬套
网上不少达梦适配的帖子是 1.x 时代写的,直接照抄到 2.x 上会翻车,差异主要在三块。
驱动加载方式不同。1.x 把 MySQL 驱动打进 fat jar,换达梦得解包、替换、重新打包,或者改pom.xml重新编译。2.x 改了加载机制,startup.sh里有一行-Dloader.path=${BASE_DIR}/plugins/mysql,用 Spring Boot 的PropertiesLauncher从外部目录加载 jar。也就是说 2.x 只需要把达梦驱动丢进plugins/mysql/目录,重启就生效,不用碰源码。这个改动大幅降低了适配成本,也是我建议新项目直接上 2.x 的原因之一。
连接池不同。2.2 之前的版本用 Druid,配置前缀是db.pool.config.透传给 Druid 的属性;2.2 之后换成了 HikariCP,前缀还是db.pool.config.,但底下的属性名完全变了。老教程里写的db.pool.config.validationQuery、db.pool.config.testWhileIdle这些 Druid 专属项,在 HikariCP 下会被静默忽略——不报错,但也不生效,你以为配了连接保活,实际上连接早被达梦服务端掐了。
表结构不同。2.x 多了config_info_gray这张灰度表,还有config_info_beta、config_info_tag里的字段变化。照着 1.x 的脚本建库,启动不报错,但一用灰度发布就报“表不存在”。所以建表脚本必须从你实际使用的那个版本的mysql-schema.sql转,别从网上抄。
3. 改造落地:配置、驱动、建表脚本一次改到位
这一章是能直接抄作业的部分。三步走完,Nacos 就能连上达梦并正常启动。
3.1 驱动包放进 plugins/mysql
先把达梦驱动 jar 拷到 Nacos 安装目录下的plugins/mysql/。这个目录名有点误导,它跟 MySQL 没绑定关系,只是历史命名,放任何 JDBC 驱动都行。
# 假设 Nacos 解压在 /opt/nacos cd /opt/nacos mkdir -p plugins/mysql # 把达梦驱动拷进去,文件名保持原样即可 cp /path/to/DmJdbcDriver18.jar plugins/mysql/ ls -l plugins/mysql/放完之后确认两件事。一是startup.sh里确实有-Dloader.path指向这个目录,不同小版本的写法略有差异,有的是${BASE_DIR}/plugins/mysql/,有的是${BASE_DIR}/plugins/mysql,末尾斜杠有无一般不影响,但如果目录压根没被加进去,驱动就加载不到。二是启动脚本不要用nacos-server.jar直接java -jar起,那样会绕过PropertiesLauncher,外部 jar 全部失效。用sh startup.sh -m standalone或者照着脚本里的完整java命令起。
提示:达梦驱动依赖
com.dameng:DmJdbcDriver18这一个包就够,不需要额外引 Oracle 的兼容包。如果你在 Docker 里跑,用 volume 把plugins/mysql挂进去,镜像不用重打。
3.2 application.properties 里要改的六处
配置文件在conf/application.properties。改动集中在这几行,其余保持默认。
# 1. 数据源平台标识,纯开关,填 dm 便于识别 spring.datasource.platform=dm # 2. 数据源数量 db.num=1 # 3. 达梦连接串,schema 指定默认模式 db.url.0=jdbc:dm://127.0.0.1:5236?schema=NACOS&connectTimeout=3000&socketTimeout=60000 # 4. 账号密码 db.user.0=NACOS db.password.0=YourPassword # 5. 驱动类,2.2 之后走 HikariCP,仍是这个前缀 db.pool.config.driverClassName=dm.jdbc.driver.DmDriver # 6. 连接池基本参数,按实际并发调 db.pool.config.maximumPoolSize=20 db.pool.config.minimumIdle=5 db.pool.config.connectionTimeout=3000逐项说一下为什么这么写。db.url.0里的connectTimeout=3000是连接超时,单位毫秒,达梦在网络抖动时握手会慢,设太小会频繁建连失败,设太大启动阶段会卡住,3000 是个折中。socketTimeout=60000是读超时,Nacos 的配置内容可能很大,content是 CLOB 字段,读大配置的时候别把超时设得太激进。schema=NACOS建议显式带上,虽然可以用db.user.0的默认模式,但一旦 DBA 给你换了个默认模式,所有表都找不到了。
maximumPoolSize这个值跟 Nacos 的线程模型有关。2.x 的配置读写走的是长连接 + gRPC,实际落到数据库的并发并不高,20 个连接对中小规模集群足够。但如果你的 Nacos 管了几千个实例、配置变更频繁,可以往上调到 50,同时确认达梦服务端的MAX_SESSIONS参数够用。
还有一处容易被忽略:Nacos 2.x 默认开启了 Derby 的初始化逻辑,切外部库之后要把spring.sql.init相关的项确认一遍,别让它在启动时又去找 Derby 脚本。具体做法是把conf/下跟 derby 有关的配置块注释掉,或者干脆把data/derby-data目录清掉,眼不见心不烦。
3.3 建表脚本从 MySQL DDL 转达梦 DDL
拿mysql-schema.sql逐表转。以最核心的config_info为例,MySQL 版本是这样的:
CREATE TABLE `config_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `data_id` varchar(255) NOT NULL, `group_id` varchar(128) DEFAULT NULL, `content` longtext NOT NULL, `md5` varchar(32) DEFAULT NULL, `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `src_user` text, `src_ip` varchar(50) DEFAULT NULL, `app_name` varchar(128) DEFAULT NULL, `tenant_id` varchar(128) DEFAULT '', `c_desc` varchar(256) DEFAULT NULL, `c_use` varchar(64) DEFAULT NULL, `effect` varchar(64) DEFAULT NULL, `type` varchar(64) DEFAULT NULL, `c_schema` text, `encrypted_data_key` text NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;转成达梦版本:
CREATE TABLE CONFIG_INFO ( ID BIGINT IDENTITY(1,1) NOT NULL, DATA_ID VARCHAR(255) NOT NULL, GROUP_ID VARCHAR(128) DEFAULT NULL, CONTENT CLOB NOT NULL, MD5 VARCHAR(32) DEFAULT NULL, GMT_CREATE TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, GMT_MODIFIED TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, SRC_USER CLOB, SRC_IP VARCHAR(50) DEFAULT NULL, APP_NAME VARCHAR(128) DEFAULT NULL, TENANT_ID VARCHAR(128) DEFAULT '', C_DESC VARCHAR(256) DEFAULT NULL, C_USE VARCHAR(64) DEFAULT NULL, EFFECT VARCHAR(64) DEFAULT NULL, TYPE VARCHAR(64) DEFAULT NULL, C_SCHEMA CLOB, ENCRYPTED_DATA_KEY CLOB NOT NULL, CONSTRAINT PK_CONFIG_INFO PRIMARY KEY (ID), CONSTRAINT UK_CONFIGINFO_DATAGROUPTENANT UNIQUE (DATA_ID, GROUP_ID, TENANT_ID) );改动点一条条对:AUTO_INCREMENT换成IDENTITY(1,1),这是达梦的自增语法,前一个参数是种子,后一个参数是步长;longtext和text一律换成CLOB;datetime换成TIMESTAMP;反引号全部去掉;ENGINE=InnoDB DEFAULT CHARSET=utf8整段删掉;UNIQUE KEY xxx (...)换成CONSTRAINT xxx UNIQUE (...),因为达梦的约束必须显式命名,不命名的话后面报错信息里只有一串系统生成的乱码,排查起来很痛苦。
引号策略必须统一,这是最容易埋雷的地方。达梦建库时有个CASE_SENSITIVE参数,默认是敏感的。表名和列名不加双引号时,达梦会自动转成大写再匹配;一旦你加了双引号,就变成精确匹配,"config_info"和CONFIG_INFO会被当成两张表。所以要么全不加引号(推荐,跟 Nacos 的 mapper 里写小写表名能对上),要么全部大写加引号。最怕的是建表脚本从网上抄了一半,一半加引号一半没加,启动时表能建出来,一查就报“表或视图不存在”。
group_capacity、his_config_info、config_info_beta、config_info_gray、tenant_info、users、roles、permissions这几张表也是同样的转法。his_config_info有个小坑:它的自增列是nid而不是id,主键是(id, nid),转的时候别把主键搞反。users、roles、permissions这三张是鉴权表,如果不开鉴权可以晚点建,但开了nacos.core.auth.enabled=true就必须先建好,否则登录直接报错。
脚本写完之后,先手动跑一遍:
# 用达梦自带的 disql 或任意客户端执行 disql NACOS/YourPassword@127.0.0.1:5236 SQL> start /opt/nacos/conf/dm-schema.sql SQL> select count(*) from user_tables where table_name like 'CONFIG%';看到表都建出来了,再启动 Nacos。这一步顺序别搞反,先启服务再建表,日志里会刷一屏的“表不存在”,看着吓人。
4. 核心代码拆解:方言、分页、大字段三处硬骨头
配置和脚本搞定,Nacos 大概率能启动了。但能启动不等于能用——配置发布、实例注册、历史查询这些操作会陆续暴露出 SQL 方言问题。这一章拆三个最典型的。
4.1 DatabaseIdProvider:给 dm 单独一份 statement
最省事的做法当然是直接改 mapper XML 里那些 MySQL 语法。但 Nacos 的 mapper 文件不少,改完之后下次升级合并代码,冲突能让你怀疑人生。更稳的做法是用 MyBatis 的databaseId机制,让框架在运行期自己挑 SQL。
原理是VendorDatabaseIdProvider会读取DatabaseMetaData.getDatabaseProductName(),达梦返回的是DM DBMS,MySQL 返回MySQL。你把这个映射关系注册进去,MyBatis 加载 mapper 时就会按databaseId过滤 statement。
@Bean public DatabaseIdProvider databaseIdProvider() { VendorDatabaseIdProvider provider = new VendorDatabaseIdProvider(); Properties properties = new Properties(); // key 是驱动返回的数据库产品名,value 是 mapper 里写的 databaseId properties.setProperty("DM DBMS", "dm"); properties.setProperty("MySQL", "mysql"); provider.setProperties(properties); return provider; }然后在 SqlSessionFactory 的构建里挂上这个 provider。Nacos 用的是 MyBatis-Spring,通常在MybatisConfig这类配置类里注册SqlSessionFactoryBean,加一行factory.setDatabaseIdProvider(databaseIdProvider())即可。
mapper XML 里则可以同一个id写两份:
<!-- MySQL 版本,保持原样不动 --> <update id="updateConfigInfo" databaseId="mysql"> INSERT INTO config_info (data_id, group_id, content, md5, gmt_create, gmt_modified) VALUES (#{dataId}, #{groupId}, #{content}, #{md5}, now(), now()) ON DUPLICATE KEY UPDATE content = #{content}, md5 = #{md5}, gmt_modified = now() </update> <!-- 达梦版本,用 MERGE INTO 替代 ON DUPLICATE KEY UPDATE --> <update id="updateConfigInfo" databaseId="dm"> MERGE INTO config_info t USING (SELECT #{dataId} AS data_id, #{groupId} AS group_id FROM dual) s ON (t.data_id = s.data_id AND t.group_id = s.group_id) WHEN MATCHED THEN UPDATE SET t.content = #{content}, t.md5 = #{md5}, t.gmt_modified = CURRENT_TIMESTAMP WHEN NOT MATCHED THEN INSERT (data_id, group_id, content, md5, gmt_create, gmt_modified) VALUES (#{dataId}, #{groupId}, #{content}, #{md5}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) </update>逻辑说明一下。VendorDatabaseIdProvider的properties里,key 必须和驱动返回的产品名严格一致,大小写敏感,DM DBMS中间是一个空格,写错就匹配不上,所有databaseId="dm"的 statement 都不会加载。挂上 provider 之后,MyBatis 加载语句时先看有没有databaseId属性:有且匹配当前库,加载;有但不匹配,跳过;没有databaseId的,如果同 id 已经有语句加载进来了,也会跳过。所以同一个 id 写两份,一份带databaseId="dm",一份不带(或者带mysql),达梦环境下自然选中前者。
MERGE INTO这段是重点。达梦不支持 MySQL 的ON DUPLICATE KEY UPDATE,得用MERGE INTO ... USING ... ON ... WHEN MATCHED THEN UPDATE ... WHEN NOT MATCHED THEN INSERT。USING后面必须跟一个行集,单值场景用SELECT ... FROM dual造一行,这是达梦支持的写法。注意ON里的条件必须和唯一约束的字段对齐,否则会出现匹配不上、疯狂插入重复数据的后果——这个坑我后面还会提。另外MERGE语句里的gmt_create只在NOT MATCHED分支赋值,MATCHED分支不能改它,写了会报语法错。
4.2 分页改写与自增主键
Nacos 的配置列表、历史查询都会分页。MySQL 写法是LIMIT #{start}, #{pageSize},达梦在 MySQL 兼容模式下能认,非兼容模式下不认。稳妥做法是改成ROW_NUMBER()或者达梦的ROWNUM。
<!-- 达梦分页:ROWNUM 两层嵌套 --> <select id="selectConfigInfoList" databaseId="dm" resultMap="configInfoResultMap"> SELECT * FROM ( SELECT t.*, ROWNUM AS RN FROM ( SELECT id, data_id, group_id, tenant_id, app_name, md5, gmt_create, gmt_modified FROM config_info WHERE tenant_id = #{tenantId} ORDER BY id DESC ) t WHERE ROWNUM <= #{offset} + #{pageSize} ) WHERE RN > #{offset} </select>这里要注意三点。第一,ROWNUM的过滤必须在内层先做<= offset + pageSize,外层再筛> offset,顺序反了会一行都没有。第二,XML 里<和>必须转义成<和>,否则 XML 解析直接失败,报错信息指向行号,但真实原因是字符没转义。第三,内层子查询里最好显式列出字段,别写SELECT *,CONTENT是 CLOB 字段,分页列表根本不需要它,选出来白白拉一大坨数据,列表接口会慢得离谱。
自增主键也有讲究。达梦的IDENTITY列在插入时不能显式赋值,除非打开SET IDENTITY_INSERT 表名 ON。Nacos 的插入语句大多数不写id,靠数据库生成,这没问题。但如果你的 mapper 里某条 SQL 为了兼容历史数据显式写了id,在达梦上就会直接报错。检查方法很简单,把 mapper 里所有INSERT语句过一遍,看字段列表里有没有自增列;有的话要么去掉,要么在外层包一个SET IDENTITY_INSERT—— 后者不推荐,事务里频繁开关自增插入会锁表。
-- 万一必须显式插入自增列,这样开 SET IDENTITY_INSERT CONFIG_INFO ON; INSERT INTO CONFIG_INFO (ID, DATA_ID, GROUP_ID) VALUES (1001, 'demo', 'DEFAULT_GROUP'); SET IDENTITY_INSERT CONFIG_INFO OFF;SET IDENTITY_INSERT是会话级的,只影响当前连接,而且同一时刻一张表只允许一个会话开着,并发场景下会互相阻塞。所以能不用就不用。
4.3 CLOB 大字段与 TypeHandler
content、src_user、c_schema、encrypted_data_key这几列都是 CLOB。达梦驱动读 CLOB 的方式和 MySQL 不太一样,MySQL 的longtext用rs.getString()能直接拿到字符串,达梦有时候返回的是Clob对象,getString()出来是dm.jdbc.driver.DmdbClob@1a2b3c这种鬼东西。
现象是配置详情接口返回一坨内存地址,MD5 校验失败,客户端拉不到配置内容。原因是 MyBatis 的StringTypeHandler调的是rs.getString(columnName),某些达梦驱动版本对 CLOB 列不会自动转成 String。
解决办法有两个。一是在 resultMap 里给 CLOB 字段显式指定ClobTypeHandler:
<resultMap id="configInfoResultMap" type="com.alibaba.nacos.config.server.model.ConfigInfo"> <id column="ID" property="id" jdbcType="BIGINT"/> <result column="DATA_ID" property="dataId" jdbcType="VARCHAR"/> <!-- 关键:CLOB 字段指定 TypeHandler,避免读出对象地址 --> <result column="CONTENT" property="content" jdbcType="CLOB" typeHandler="org.apache.ibatis.type.ClobTypeHandler"/> <result column="MD5" property="md5" jdbcType="VARCHAR"/> </resultMap>二是升级达梦驱动到较新的小版本,驱动层已经修掉了不少 CLOB 自动转换的问题。两个办法可以叠加使用,先改 resultMap 最稳,不动驱动版本。
参数说明:jdbcType="CLOB"告诉 MyBatis 这列按大字段处理,typeHandler指定用ClobTypeHandler,它内部会调rs.getClob()再读字符流。如果你的实体字段是String类型(Nacos 的ConfigInfo.content就是String),ClobTypeHandler能正确转过去,不用改实体类。
写入方向也要注意。插入时content可能很大(Nacos 单条配置上限默认 10KB,改过限制的可能更大),达梦驱动在setString到 CLOB 列时,超过一定长度会自动转成流写入,这个不用特殊处理。但如果你的 MyBatis 参数类型没标jdbcType=CLOB,某些驱动版本会按VARCHAR处理,超长直接报“字符串截断”。所以插入语句里的#{content}最好也带上jdbcType:
INSERT INTO config_info (data_id, group_id, content, md5, gmt_create, gmt_modified) VALUES (#{dataId}, #{groupId}, #{content,jdbcType=CLOB}, #{md5}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)5. 排查与避坑:六条从日志里抠出来的记录
适配过程中踩的坑,基本集中在启动和运行两个阶段。下面六条按「现象 → 原因 → 解决」写,都是我实际遇到过的。
5.1 启动阶段的三条
第一条:java.lang.ClassNotFoundException: dm.jdbc.driver.DmDriver。
现象是启动脚本一跑就抛这个异常,日志在logs/start.out的最前面。原因是驱动 jar 没被加载到 classpath 上。要么 jar 没放进plugins/mysql/,要么放了但启动命令没用-Dloader.path指向该目录,要么用了java -jar nacos-server.jar这种绕过PropertiesLauncher的起法。解决方式:ls plugins/mysql/确认 jar 在,ps -ef | grep nacos看启动参数里有没有-Dloader.path,然后统一用startup.sh启动。另外 jar 包别改名成mysql-connector.jar之类的,保持原名,省得自己都分不清放的是哪个。
第二条:表或视图不存在,报错里表名是大写。
现象是启动到一半,日志刷Table or view does not exist: CONFIG_INFO,但你去数据库里查,表明明建好了。原因是引号策略不统一:建表脚本用了双引号小写,运行期 mapper 里写的是不带引号的小写表名,达梦自动转大写去匹配,匹配不上。或者反过来,建表用了引号大写,mapper 里写小写。解决方式:建表脚本全部去掉双引号,让达梦自动转大写;mapper 里的表名小写也没关系,不带引号时同样会转大写。两边都“不引号 + 自动大写”,永远不会错。已经建错的,把表删了重建,别试图用ALTER改大小写,达梦改不了。
第三条:连接池初始化失败,报active: 0, maxActive: 20之后就卡住。
现象是启动日志停在数据源初始化,几十秒后超时退出。原因是连接池的driverClassName没配或者配成了 MySQL 的com.mysql.cj.jdbc.Driver。2.2 之后用 HikariCP,如果db.pool.config.driverClassName不写,它会尝试从 URL 推断,达梦的jdbc:dm://前缀它不认识,推断失败就卡住。解决方式:显式写上db.pool.config.driverClassName=dm.jdbc.driver.DmDriver。同时确认db.pool.config.connectionTestQuery别配成select 1——达梦支持SELECT 1 FROM DUAL,但select 1在某些版本下会报语法错,干脆不配,HikariCP 会用 JDBC4 的isValid()检测。
5.2 运行阶段的三条
第四条:配置发布成功,但列表页查不到,或者同一条配置出现多份。
现象是点发布没报错,刷新列表为空,多刷几次又冒出来几条重复的。原因基本都指向MERGE INTO的ON条件和唯一约束没对齐。比如config_info的唯一键是(data_id, group_id, tenant_id),你的MERGE的ON只写了(data_id, group_id),那么不同tenant_id的记录会被判定为“已存在”,全部走 UPDATE,结果就是租户隔离失效、数据被覆盖。反过来,如果ON写的条件比唯一键更严格,就会匹配不上,每次都走 INSERT,产生重复数据。解决方式:ON条件必须和表上的唯一约束字段完全一致,一个不多一个不少。改完之后把已有的脏数据清掉,不然历史重复记录会一直干扰查询。
第五条:分页查出来的数据量不对,第一页正常,第二页开始错位。
现象是配置列表翻页,第一页 10 条,第二页出来的和第一页有重叠,或者干脆空页。原因是分页 SQL 的ROWNUM嵌套写反了,或者ORDER BY放在了外层。ROWNUM是在结果集生成过程中逐行分配的,如果ORDER BY和ROWNUM在同一层,排序发生在ROWNUM分配之后,翻页顺序就乱了。解决方式:ORDER BY必须放在最内层子查询里,ROWNUM过滤放在中间层,最外层再按RN筛选。也就是我上面 4.2 里那种三层结构,一层都不能少。另外offset和pageSize的类型要是int,传成long在某些驱动版本下会拼出ROWNUM <= 10.0这种非法 SQL。
第六条:历史配置查询报字符串截断或返回内容为空。
现象是查his_config_info表,content字段要么空,要么报截断。原因是这张表的content在 MySQL 里是longtext,转达梦时有人图省事写成了VARCHAR(4000),超过 4000 字节的配置直接截断或报错。解决方式:his_config_info、config_info_beta、config_info_gray里所有对应longtext/text的列,一律用CLOB,别用VARCHAR凑合。VARCHAR在达梦里最大 8188 字节,看着够用,但 Nacos 的配置内容加上转义字符很容易超,而且超了之后报错信息不明显,等到线上出问题就晚了。
注意:改完表结构之后,
his_config_info里的历史数据不会自动补全,被截断的部分找不回来。所以建表这步一定要在正式使用前做完,别等跑了一段时间再回头改。
6. 二次开发:把改造固化成一份可复用补丁
改完一个环境,下一个环境还得再来一遍,这事儿我干过三次之后就受不了了。后来把整套改动整理成一个补丁目录,新环境部署时直接覆盖,十分钟搞定。
结构大概是这样:
nacos-dm-patch/ ├── conf/ │ ├── application.properties # 数据源配置片段 │ └── dm-schema.sql # 达梦建表脚本 ├── plugins/ │ └── mysql/ │ └── DmJdbcDriver18.jar # 驱动 ├── mapper/ │ ├── ConfigInfoMapper_dm.xml # 方言增量 │ └── HisConfigInfoMapper_dm.xml └── apply.sh # 一键覆盖脚本apply.sh干的事很简单:备份原始conf/application.properties,把配置片段追加进去,把驱动拷到plugins/mysql/,把 mapper 增量文件拷到 Nacos 安装目录的plugins/mapper/下(这需要你在application.properties里加一行mybatis.mapper-locations=classpath*:mapper/*.xml,file:${nacos.home}/plugins/mapper/*.xml,让 MyBatis 同时扫外部目录)。这样升级 Nacos 版本时,只要mysql-schema.sql没大改,补丁原样能用。
验证方法我一般按这个顺序走,每一步都能卡住一类问题:
| 步骤 | 操作 | 通过标准 |
|---|---|---|
| 1 | sh startup.sh -m standalone | 日志出现Nacos started successfully |
| 2 | 打开控制台,看配置列表 | 列表能加载,不报 500 |
| 3 | 新建一条配置并发布 | 返回 200,列表出现该条 |
| 4 | 编辑该配置,改三次 | 历史版本里有三条记录,内容完整 |
| 5 | 注册一个临时实例 | 服务列表能看到,健康状态为UP |
| 6 | 重启 Nacos | 配置和实例都还在 |
第 4 步是关键,它同时验证了MERGE INTO、CLOB 读写、历史表自增三件事。第 6 步验证的是持久化真的落了库,而不是还在内存或 Derby 里。
有个技巧值得说:ConfigInfoMapper的增量文件我一般不全量重写,只放databaseId="dm"的那几条 statement,原文件一个字不动。这样 MySQL 环境照样能跑,同一份代码双库兼容。代价是 MyBatis 启动时会为每条语句做一次databaseId匹配,启动时间多几十毫秒,可以忽略。
最后说个教训。早期我做适配,图快,直接改原 mapper 文件,把LIMIT换成ROWNUM,本地跑通就上线了。结果三个月后 Nacos 出了个小版本升级,合并代码时几十个冲突,改过的 SQL 全得手工比对,其中一条ON DUPLICATE KEY UPDATE被我漏改,上线后配置写入全部变成新增,一天之内config_info表涨了十几万条重复记录,清理脚本写了俩小时。从那以后我每次做数据库适配,都强制走一遍「新增 databaseId 语句、不动原文件、留补丁目录」这套流程,哪怕项目再小也不省这一步。希望帮到你。
本文还有配套的精品资源,点击获取