十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

达梦数据库关键字屏蔽:敏感词过滤与保留字冲突拆解

达梦数据库关键字屏蔽:敏感词过滤与保留字冲突拆解 很多团队第一次在达梦数据库上栽跟头不是因为 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_TEXTtype换成CATEGORY_CODEenabled换成ENABLED_FLAGkey这种词干脆不用。加后缀的方式比加前缀更好读而且能保证同一个库里命名风格统一。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_INCREMENTIDENTITY(1,1)放在列类型后面ENGINEInnoDB直接删掉DM 无此语法DEFAULT CHARSETutf8mb4直接删掉字符集在建库时定DATETIMETIMESTAMP或DATETIME视版本兼容性而定TEXTCLOB或VARCHAR视长度而定ON UPDATE CURRENT_TIMESTAMP用触发器实现DM 不直接支持该子句TINYINT(1)TINYINT长度参数无意义反引号包的库名模式名SCHEMADM 用模式管理这张表里最麻烦的是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复杂度从词数 × 文本长度降到文本长度 × 树深度词库越大优势越明显。实现上有两个细节值得展开。第一节点结构用MapCharacter, Node还是数组取决于字符集范围中文词库用HashMap更省内存纯英文词库可以用定长数组换速度。第二终止标记不要只用一个布尔值我建议存一个SetString或者词条 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 USERIDSYSDBA/SYSDBA127.0.0.1:5236 \ FILEkw_full_20240101.dmp \ DIRECTORY/dm/backup \ SCHEMASAPP \ LOGexp_full.log # 只导出词库相关表 dexp USERIDSYSDBA/SYSDBA127.0.0.1:5236 \ FILEkw_table_20240101.dmp \ DIRECTORY/dm/backup \ TABLESAPP.T_KEYWORD_LIB \ LOGexp_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和高频命中内容结合起来定期反哺词库把明显误判的词下掉、把绕过率高的变体补进去。这个闭环做起来不难难的是坚持更新毕竟词库这东西建起来容易养起来靠的是耐心。
返回列表