很多团队第一次在达梦数据库上栽跟头,不是因为 SQL 写错,而是因为建表语句里那个字段名叫value。我最近在一个内容风控项目里就撞上这事:原本跑在 MySQL 上的关键字屏蔽模块要整体迁到达梦,DDL 一执行,报错信息含糊得像谜语,排查了半小时才发现是保留字冲突。而更麻烦的是,"关键字屏蔽"这个词在我们的需求文档里本身就指两件事——一是业务上要过滤敏感词的屏蔽功能,二是数据库层面要绕开保留关键字的屏蔽。这两件事经常被混在一起讨论,结果就是词库表设计得别扭,SQL 写得提心吊胆。这篇文章就把这两条线彻底拆开讲清楚:词库怎么建模、DM 的保留字怎么绕、过滤算法怎么落、以及安装连接备份这些配套动作里容易忽略的细节。适合正在做国产化适配、或者准备把风控/评论过滤模块搬到达梦上的同学参考,新手可以直接抄表结构和代码,有经验的可以只看排错和取舍那几段。
1. 一次建表报错引出的两条主线:敏感词过滤与保留字冲突
1.1 同名不同义:先把"关键字屏蔽"拆开
先把概念对齐,不然后面全是无效沟通。业务方嘴里的"关键字屏蔽",指的是用户发帖、评论、昵称里出现违禁词时,系统要做拦截、替换成星号或者丢进人工审核队列;这是功能层的需求。而开发和 DBA 嘴里的"关键字屏蔽",往往指的是写 SQL 时字段名、表名、变量名撞上了数据库的保留字,导致语法报错或者行为异常;这是语法层的问题。
同一个项目里这两件事会同时出现,因为屏蔽功能天然要维护一张词库表,而词库表最容易起的字段名就是value、type、level、key这几个——恰好全是保留字。我在项目里的实际做法是:把语法层的问题在建模阶段一次性解决掉,把功能层的问题独立成一个可插拔的模块,两者之间的耦合点只有一张表和一个查询接口。这样后面即使词库从一万条涨到两百万条,或者要换数据库,改动面也是可控的。
提示:需求评审时如果对方只说"做个关键字屏蔽",一定追问一句是过滤用户内容还是处理 SQL 命名,否则大概率返工。
1.2 为什么这个坑总在迁移到 DM 之后才爆
原因很现实:MySQL 对标识符的宽容度太高了。用反引号包起来,value、type、level想叫什么叫什么,很多人写了两三年都没意识到这些是关键字。到了达梦,情况变了。达梦对标识符的处理更接近标准 SQL 的严格风格,保留字清单长,而且大小写敏感策略直接决定了双引号能不能救你,这就不是一个反引号换双引号能解决的问题了。
我在迁移时遇到的具体现象是:建表没问题(因为脚本里带了引号),但应用里的 MyBatis 映射没带引号,跑起来报"无效的列名",而这个报错在日志里被业务异常盖住了,查了半天以为是连接池问题。所以我的经验是,迁移 DM 时不要只跑通一条查询就认为完事,要把所有涉及该表的 SQL都过一遍,包括分页、排序、动态条件拼接。尤其是动态 SQL 里order by value这种写法,平时不报错,一到排序就炸。
另外要说清楚一点:达梦提供COMPATIBLE_MODE之类的兼容参数,按兼容模式初始化实例确实能容忍一部分异库语法。但我个人不建议把兼容模式当成解决方案,它适合作为迁移过渡期的缓冲,长期还是应该把 SQL 写干净,否则一旦实例重建、参数被调整,隐患立刻暴露。
2. 屏蔽词库的表设计:字段命名、字符集与索引一起定
2.1 建表 DDL 与踩坑字段的替换方案
词库表看着简单,实际上决策点不少。下面这份是我在项目里实际用的结构,已经把所有高风险字段名替换掉了,并且在 DM8 上验证通过:
CREATE TABLE T_KEYWORD_LIB ( ID BIGINT IDENTITY(1,1) NOT NULL, WORD_TEXT VARCHAR(64) NOT NULL, WORD_PINYIN VARCHAR(64), CATEGORY_CODE VARCHAR(32) DEFAULT 'DEFAULT', MATCH_MODE TINYINT DEFAULT 1, ENABLED_FLAG TINYINT DEFAULT 1, HIT_COUNT BIGINT DEFAULT 0, CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UPDATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT PK_KEYWORD_LIB PRIMARY KEY (ID) );几个命名上的取舍说明一下。value换成WORD_TEXT,type换成CATEGORY_CODE,enabled换成ENABLED_FLAG,key这种词干脆不用。加后缀的方式比加前缀更好读,而且能保证同一个库里命名风格统一。ID用IDENTITY(1,1)而不是 MySQL 的AUTO_INCREMENT,这是 DM 的原生写法,虽然兼容模式下也能认一部分,但既然是新表,没必要依赖兼容。
MATCH_MODE用来区分匹配策略,比如 1 表示精确命中、2 表示包含命中、3 表示正则,用一个整型编码比存字符串节省空间且索引友好。HIT_COUNT这个字段是我强烈建议加的:上线后你会发现词库里有大量永远命中不了的僵尸词,靠命中次数排序做季度清理非常有效,比凭感觉删词靠谱得多。
索引方面,至少要有WORD_TEXT上的索引,因为归一化之后要做批量比对。如果词库会按业务线隔离,那就建(CATEGORY_CODE, ENABLED_FLAG)的联合索引。注意别建太多索引,词库表写少读多,一次导入几万条时索引维护成本是实打实的。
2.2 字符集与排序:中文、全半角、大小写的归一化
这一节是很多人漏掉的,但直接决定屏蔽的漏判率。现实情况是,用户会想尽办法绕过过滤:全角写"广告"、插入空格写"广 告"、用大小写混写英文词、甚至用繁体字。如果你的词库和用户输入没有经过同一套归一化处理,命中率会掉得很难看。
我在项目里的做法是:词库存的是归一化后的形式,用户输入在比对前也做同样的归一化。具体的归一化步骤包括去空格、去常见分隔符、全角转半角、英文转小写、连续重复字符压缩。比如把"广 告"和"广告"统一成"广告"再比对,能多命中相当一部分变体。
字符集上,DM 建库时按 UTF-8 系列选择即可,中文存储没有问题。真正需要注意的是:归一化逻辑千万不要放在数据库的LIKE或函数里做,性能会很难看。放在应用层做,词库同步一份归一化结果,用户输入在内存里归一化后再去匹配,这样一次性成本可控,迭代也方便。
2.3 词库量级与索引:一万条和一百万条是两种做法
词库量级直接决定架构,别看表结构一样就以为能一套代码用到底,实际差别非常大:
| 词库量级 | 推荐匹配方式 | 加载策略 | 备注 |
|---|---|---|---|
| 一万条以内 | 全量加载进内存做前缀树 | 启动加载,定时全量刷新 | 最简单,够用 |
| 十万条左右 | 前缀树 + 首字索引分片 | 启动加载,增量刷新 | 注意堆内存预留 |
| 百万条以上 | 前缀树 + 敏感度分层,冷词落库 | 热词常驻,冷词按需查 | 需要命中日志反哺 |
一万条以内其实怎么做都对,直接select全表塞进Set或者 DFA 都行。到了十万条级别,就要开始关心单机内存和刷新时的抖动问题。我踩过的坑是:早期用定时任务每分钟全量刷新,词库涨到八万条之后,每次刷新都会造成一两百毫秒的停顿,高峰期偶发超时。后来改成版本号对比 + 仅在版本变化时刷新,问题就消失了。
百万级词库我目前还没在单机内存里硬扛过,思路一般是分层:高频词(近 30 天有命中的)常驻内存,长尾词放在 DM 里,用前缀做粗筛之后再去库里精查。这个方案的关键是前缀列的索引设计,以及把查询次数压下来——否则数据库压力会转移成瓶颈。
3. DM 保留字清单怎么查、SQL 里怎么绕
3.1 用 V$RESERVED_WORDS 自查,比背清单靠谱
网上的保留字清单版本很杂,不同大版本之间还会变,背下来意义不大。达梦提供了系统视图可以直接查,我通常这么用:
SELECT KEYWORD, LENGTH, RESERVED FROM V$RESERVED_WORDS WHERE RESERVED = 'Y' ORDER BY KEYWORD;不同版本这个视图的字段名可能略有差异,以你现场环境的实际结果为准,但"能自查"这件事本身就够了。我的习惯是把这份清单导出来存成项目里的一个文本文件,每次要新增表或字段时,先拿字段名去比对一遍。这个动作看起来笨,但比我见过的大多数"凭感觉"要省时间得多。
常见中招的字段名,我整理了一份高频清单,可以直接拿去改:VALUE、TYPE、LEVEL、SIZE、COMMENT、KEY、POSITION、ORDER(尤其ORDER_NO是安全的,但单独的ORDER危险)、GROUP、USER、YEAR、MONTH、DAY、TIME、DATE、NAME在部分场景下也要小心、PASSWORD这类看似普通的词也可能在函数参数场景里冲突。改法就是加业务前缀或后缀,而不是加引号硬扛。
3.2 双引号不是万能药:DM 的大小写敏感是前提
这里是最容易翻车的地方,必须讲透。达梦在初始化实例时有一个"是否大小写敏感"的选项,这个选项一旦定下来,后期改不了,除非重建实例。它决定了三件事:
- 大小写不敏感(部分环境默认,或迁移时为省事特意选的)时,不加引号的标识符统一按大写处理,加引号则保留原样。此时你写
"value"和VALUE会被当成不同的对象,很反直觉。 - 大小写敏感时,不加引号的标识符仍然按大写处理,加了双引号的小写名字会精确匹配小写,后续所有引用都必须带同样大小写的引号。
- 无论哪种情况,用双引号包一个保留字,本质上是在创建一个"名字就叫关键字"的对象,能跑通,但以后每个写 SQL 的人都必须记得加引号,一旦有人漏了,报错现场就会非常难查。
所以我的结论很明确:新表一律不碰保留字,改名成本最低;只有接手既有表、改不动的时候才用引号兜底。如果确实要用引号,那就把引号写进代码规范里,并且把这类表和字段列一个清单放在 README 里,让后面接手的人一眼能看到。
3.3 从 MySQL 搬过来的脚本,逐项改写对照表
迁移脚本的改写工作量往往被低估。我实际处理过的对照关系大致是这样:
| MySQL 写法 | 达梦对应写法 | 说明 |
|---|---|---|
| 反引号包字段 | 双引号或直接改名 | 建议改名而非加引号 |
AUTO_INCREMENT | IDENTITY(1,1) | 放在列类型后面 |
ENGINE=InnoDB | 直接删掉 | DM 无此语法 |
DEFAULT CHARSET=utf8mb4 | 直接删掉 | 字符集在建库时定 |
DATETIME | TIMESTAMP或DATETIME | 视版本兼容性而定 |
TEXT | CLOB或VARCHAR | 视长度而定 |
ON UPDATE CURRENT_TIMESTAMP | 用触发器实现 | DM 不直接支持该子句 |
TINYINT(1) | TINYINT | 长度参数无意义 |
| 反引号包的库名 | 模式名(SCHEMA) | DM 用模式管理 |
这张表里最麻烦的是ON UPDATE CURRENT_TIMESTAMP,达梦不直接支持,得用触发器把UPDATE_TIME补上。我建议的做法是干脆不用触发器,在应用层显式赋值——逻辑集中在一处,排查问题时不用翻数据库对象,维护成本更低。触发器这种东西,写在数据库里看着优雅,出问题时就是黑盒。
另一个容易忽略的是模式(SCHEMA)的概念。MySQL 里"库"和"表"是两级,DM 里通常用"模式"来隔离业务,跨模式访问要带模式名或者配同义词。如果原来脚本里写死了库名,搬迁时要么统一建同义词,要么挨个改 SQL,两种做法各有代价,我倾向于给高频表建同义词,改动面最小。
4. 过滤算法落地:状态机、热更新与三种处置策略
4.1 为什么词库放 DM、匹配放内存
先说选型逻辑,因为很多人第一反应是"直接在 SQL 里查不就行了"。短时间看,where instr(content, word) > 0这种写法确实能跑,但问题在于:每次用户提交内容都要把全量词库和文本做一遍比对,数据库 CPU 直接被打满,而且随着词库增长,耗时是线性上涨的。内容风控这个场景的特点是读极多、写极少、延迟敏感,把它放在数据库里做计算是典型的用错地方。
正确的分工是:DM 负责持久化和版本管理,内存里的前缀树负责实时匹配。词库变更时,通过版本号或者消息通知应用重新加载,正常请求链路完全不碰数据库。这样一来,单次匹配的耗时能稳定在微秒级,跟词库是否放在达梦里都没关系了。DM 在这个架构里承担的是"权威数据源"的角色,顺带还能利用它的备份能力保证词库不丢。
4.2 DFA 前缀树的 Java 实现要点
DFA(确定有限状态自动机)做敏感词匹配是经典方案,核心思想是:把词库构建成一棵字符树,匹配时从文本的每个位置出发沿树走,走到终止节点就命中。相比对每个词做一次contains,复杂度从"词数 × 文本长度"降到"文本长度 × 树深度",词库越大优势越明显。
实现上有两个细节值得展开。第一,节点结构用Map<Character, Node>还是数组,取决于字符集范围;中文词库用HashMap更省内存,纯英文词库可以用定长数组换速度。第二,终止标记不要只用一个布尔值,我建议存一个Set<String>或者词条 ID 列表,因为同一个前缀可能对应多个词——比如"测试"和"测试词"都在词库里,走到"测试"这个节点时既是一个完整词,也是更长词的前缀,布尔值表达不了这种情况,会导致短词漏判。
还有一点关于特殊字符的处理:构建树的时候就应该把空格、标点、符号这些"干扰字符"排除掉,同时把原始文本在匹配前做同样的清洗。这样"广 告"和"广告"在树里走的是同一条路径,天然就命中了,不用写额外的变体逻辑。
4.3 词库热更新:借 Nacos 配置中心的思路做版本号刷新
热更新是这类模块的必备能力,因为运营改词库是常态,不能每次重启应用。我项目里用过两种做法,都值得说。
第一种是版本号轮询:在 DM 里维护一张T_KEYWORD_VERSION表,每次词库变更就更新版本号和时间戳。应用侧起一个低频定时任务(比如 30 秒一次),只查这张单行表,发现版本号变了才去加载全量词库。这个方案实现简单,对数据库压力几乎为零,代价是最长 30 秒的生效延迟,对风控场景通常可以接受。
第二种是结合配置中心的推送机制。如果项目里已经有配置中心在跑(很多团队用 Nacos 做配置和注册),可以把"词库版本号"这一个值放到配置中心里,词库本体仍然存在 DM。运营改词库时,写库加推配置两步,应用监听到配置变化再去加载。这样生效延迟能压到秒级甚至更低。
要注意的是,如果项目本身就是把 Nacos 接到了达梦上做持久化,在做这套适配时有几个坑:Nacos 官方支持的数据库类型有限,接达梦一般需要自己写数据源插件,并且把建表脚本改写成 DM 语法,重点是反引号、自增主键、时间类型这几处。适配脚本一定要在测试库完整跑一遍,别只跑建表就认为通了,启动过程中的初始化 SQL 才是最容易出问题的。
4.4 屏蔽、替换、拦截:返回策略怎么选
匹配出来之后怎么处理,是有产品决策成分的。我总结过三种策略的适用场景:
- 替换(打码):把命中词替换成等长的星号,用户体验最平滑,适合评论、弹幕这类公开内容区。要注意等长替换,否则字数变化会让用户察觉。
- 拦截(拒绝提交):直接返回错误提示,适合昵称、简介这类一次性提交的场景。提示语要不要告诉用户具体命中了哪个词,是个值得讨论的点:提示具体词会帮助恶意用户试探规则,我倾向于只提示"内容包含不合规信息"。
- 降级(转人工):不拒绝也不打码,直接进审核队列,适合边界模糊的场景。这类请求要打标记,方便后续统计。
这三种策略应该是可配置的,配置的粒度可以到业务线甚至具体字段,而不是写死在代码里。我的做法是词库表里的MATCH_MODE配合一份业务配置,应用根据业务标识查配置决定动作。这样运营调整策略不用发版,效率高很多。
5. 上线前后的配套动作:连接、备份与架构取舍
5.1 Linux 安装与 Navicat 连接里最容易错的两个参数
安装本身网上教程很多,我只说两个实际踩过的点。第一是初始化实例时的大小写敏感选项,前面讲过它改不了,所以必须在dminit阶段就定下来。迁移项目我一般选"大小写敏感",因为这样跟开发在 MySQL 上养成的习惯差异更可控,但代价是所有小写对象名以后都要带引号。如果团队 SQL 风格比较统一、全部用小写加引号,选敏感更安全;如果脚本里混着大小写且懒得改,选不敏感反而省事。这是个团队决策,不是技术优劣。
第二是 Navicat 连接。达梦的默认端口是 5236,不是 3306,这个要确认端口没被改过。用户名默认是SYSDBA,密码在初始化时设定,很多环境初始密码就是同名大写。连接类型要选达梦对应的驱动,如果 Navicat 版本较老没有这个选项,升级客户端或者在连接配置里手动指定 JDBC 驱动都可以。连上之后如果看不到表,先确认当前模式(SCHEMA)对不对——达梦是按模式隔离对象的,连接默认落在SYSDBA模式下的概率很大,而你的业务表在另一个模式里,这一条我见过不止一个人卡住。
顺带说一句,用客户端工具做词库维护时,建议不要直接改生产表,而是通过应用的管理接口改。原因是词库变更往往要触发缓存刷新,直接改表绕过了刷新链路,会出现"库里改了但过滤不生效"的诡异现象,排查起来非常费时间。
5.2 词库表的逻辑备份与恢复验证
词库是运营资产,丢了比丢代码还麻烦,所以备份要做,但更重要的是验证恢复。达梦的逻辑备份用dexp,我常用的命令形态是这样:
# 导出整库 dexp USERID=SYSDBA/SYSDBA@127.0.0.1:5236 \ FILE=kw_full_20240101.dmp \ DIRECTORY=/dm/backup \ SCHEMAS=APP \ LOG=exp_full.log # 只导出词库相关表 dexp USERID=SYSDBA/SYSDBA@127.0.0.1:5236 \ FILE=kw_table_20240101.dmp \ DIRECTORY=/dm/backup \ TABLES=APP.T_KEYWORD_LIB \ LOG=exp_table.log恢复用dimp,参数对应即可。这里我踩过的坑是:导出时指定了模式,恢复时目标模式不存在会直接失败。所以恢复脚本里要先判断模式、按需创建,别指望工具帮你兜住。另外别忘了验证步骤——恢复到一个临时库、条数比对、抽样查几条中文看有没有乱码,这三步走完才算备份有效。只导出不验证,本质上没有备份。
物理备份也可以用,BACKUP DATABASE之类的命令,但前提是数据库开了归档模式。词库这种小表用逻辑备份更灵活,整库物理备份作为兜底。两者不冲突,按 RPO 要求搭配着来就行。
5.3 DW 与 DSC:词库读多写少的场景怎么选
达梦的两种高可用架构经常被拿来比较,放到词库这个场景里答案其实比较清楚。DW(数据守护)本质是主备架构,主库写、备库读或者只做容灾,部署相对简单;DSC(共享存储集群)是多节点共享同一份存储的集群,读写能力都可以横向扩展。词库场景的特点是读请求主要发生在应用启动加载那一次,运行期几乎不产生查询,写请求更是低频——运营改词的量级一天可能就几十次。
在这种情况下,选 DW 就够了,没必要为了"看起来更高端"上 DSC。DW 的主备能满足容灾需求,备库还能顺手承担一些报表类查询。DSC 的价值在高并发写入和多节点同时读写,词库场景用不上,反而因为共享存储的运维复杂度带来额外风险。
真正需要关心的是应用侧缓存和数据库之间的一致性。无论选哪种架构,词库变更到应用生效之间都存在窗口期。我通常会把窗口期明确写进需求文档,跟产品对齐"改词后多久生效",避免上线后被当成 bug 反复追问。如果业务要求秒级生效,那就走配置中心推送那条路;能接受分钟级,版本号轮询足够了。
6. 联调期反复出现的几类现象与排查顺序
6.1 中文乱码、排序错乱与屏蔽漏判
联调阶段的问题大多集中在三类。第一类是乱码:导出脚本文件的编码和数据库字符集不一致,导致导入的中文变成问号。这类问题要从源头上确认编码,脚本文件统一用 UTF-8,导入工具的编码选项也要对齐,别在数据库端用函数去"修",修不回来的。
第二类是排序错乱:用ORDER BY排中文时结果和预期不一致,这跟排序规则有关,不同字符集下的中文排序顺序不同。如果业务对中文排序有要求,最好在应用层排,或者显式指定排序规则,别依赖默认行为。
第三类是漏判,也是最有迷惑性的一类。用户说"这个词明明在词库里为什么没过滤",可能的原因有:词库缓存没刷新、归一化不一致(一边去空格一边没去)、大小写处理不同、或者词库里的词带了不可见的空白字符。第三类最阴险——运营从 Excel 复制粘贴进来的词,末尾经常带一个换行或空格,肉眼完全看不出来,匹配时永远不命中。我的处理办法是在入库前统一trim并过滤控制字符,同时在管理界面上把这类词标红提示。
6.2 首拼码函数:让"拼音谐音"也能命中
中文内容的变体绕行策略里,拼音是一大类。达梦这边有两种做法生成首拼码。一种是在数据库端实现——可以写存储过程配合一份汉字码表,或者在支持的情况下使用 Java 函数;另一种是在应用层生成后写入WORD_PINYIN字段。
我倾向于后者,理由是:码表维护在应用侧更灵活,升级换版本不用动数据库;而且生成逻辑只在词库变更时执行一次,性能不是问题。数据库端做这件事的收益仅限于"其他系统也能复用",如果只有一个应用用,就不值得。
存了首拼码之后,匹配流程变成两级:先用原词匹配,没命中再用输入文本的首拼去匹配词库的首拼列。这个方案能覆盖相当一部分谐音变体,但要注意误伤风险——短的拼音组合很容易撞上正常词,所以首拼匹配一般只用于"降级处理"(转人工审核)而不是直接拦截。我见过直接拦截导致大量正常内容被误伤的案例,调整策略花的沟通成本比省下的审核成本高得多。
6.3 我常用的三段式排查顺序
最后分享一套排查顺序,基本能覆盖这个模块 90% 的问题。第一步先确认词库本身:直接在 DM 里查这个词在不在、ENABLED_FLAG是不是 1、有没有隐藏字符。这一步能排掉一半问题,而且是成本最低的。
第二步确认缓存:看应用的词库版本号和数据库里的版本号是否一致,不一致就是刷新链路的问题,检查定时任务是否在跑、配置中心的监听是否生效。这里有个细节:应用多实例部署时,有可能其中一个实例刷新成功另一个失败,表现为"偶发不生效",所以查看版本号要按实例看,不能只看一个。
第三步才怀疑代码逻辑:归一化函数有没有被改过、匹配入口是不是走的新实现、有没有别的过滤器在前置环节把内容截断了。这一步成本最高,所以放最后。
这套顺序的核心逻辑是:按成本从低到高排,而不是按可能性大小排。很多时候我们凭直觉觉得"肯定是代码 bug",结果查了半天发现是词库里的一个空格。我在实际项目里养成的一个习惯是,每次新增一条屏蔽规则,都先在测试环境用一条构造好的样例内容验证一遍再上线,这个动作花不了两分钟,但能省掉大量线上排查时间。
至于后续可以怎么扩展,我个人比较想加的是命中统计的闭环——把HIT_COUNT和高频命中内容结合起来,定期反哺词库,把明显误判的词下掉、把绕过率高的变体补进去。这个闭环做起来不难,难的是坚持更新,毕竟词库这东西,建起来容易,养起来靠的是耐心。