3、特征注册表设计:MySQL表结构设计、核心字段定义与索引策略

特征注册表是整个特征平台的「元数据中心」。说白了,它不存特征值,只存特征的「身份证信息」——叫什么名字、什么类型、从哪来、现在什么状态。我见过不少团队一上来就急着写特征计算逻辑,结果特征一多,命名乱套、类型冲突、找不到归属……嗯,这时候再回头补注册表,成本就高了。

这一章,我们就来聊聊怎么把这张表设计得既干净又扛打。

3.1 核心字段定义:一张表管住所有特征元数据

先上我最常用的一张表结构。它经历过日均百万级特征调用的考验,字段不多,但每个都有存在的理由。

CREATE TABLE `feature_registry` (
  `id`            BIGINT       NOT NULL AUTO_INCREMENT  COMMENT '自增主键',
  `feature_id`    VARCHAR(64)  NOT NULL                 COMMENT '特征唯一标识,业务含义编码',
  `feature_name`  VARCHAR(128) NOT NULL                 COMMENT '特征中文名/可读名',
  `feature_type`  VARCHAR(32)  NOT NULL                 COMMENT '数据类型:INT, FLOAT, STRING, BOOLEAN, VECTOR...',
  `status`        TINYINT      NOT NULL DEFAULT 0       COMMENT '状态:0-草稿, 1-启用, 2-废弃, 3-灰度',
  `source_type`   VARCHAR(32)  NOT NULL                 COMMENT '数据来源:HIVE, KAFKA, HTTP, DB, FILE',
  `source_config` JSON                                  COMMENT '来源配置,如Hive表名/Kafka topic',
  `owner`         VARCHAR(64)  NOT NULL                 COMMENT '负责人工号或团队名',
  `description`   VARCHAR(512)                          COMMENT '特征含义说明',
  `tags`          JSON                                  COMMENT '标签数组,用于分类/检索',
  `version`       INT          NOT NULL DEFAULT 1       COMMENT '乐观锁版本号',
  `created_at`    DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at`    DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_feature_id` (`feature_id`),
  KEY `idx_status_type` (`status`, `feature_type`),
  KEY `idx_owner` (`owner`),
  KEY `idx_updated_at` (`updated_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='特征注册表';

我来拆开讲讲几个关键字段的设计思路。

feature_id:特征的「身份证号」

我个人习惯用带业务含义的编码,比如 user_age_online_v2 或 order_amt_30d_sum。为什么不用自增ID?因为特征会被上下游大量引用——模型训练脚本、特征计算任务、在线服务配置……如果只用数字ID,人根本记不住,排查问题得来回查表。feature_id 一旦发布,禁止修改。我见过有人手滑改了个ID,结果线上模型加载失败,回滚花了两个小时。

⚠️ 避坑:feature_id 长度建议控制在 64 字符以内。太长了不仅浪费索引空间,而且在一些 RPC 框架中,特征名会被序列化到请求头,过长可能触发协议限制。

feature_type:类型即契约

特征类型不只是给人类看的,它直接决定了在线服务怎么反序列化、怎么做校验。我建议至少支持这几种:

类型值 说明 典型场景
INT 整型 年龄、次数、等级
FLOAT 浮点型 金额、概率、评分
STRING 字符串 城市、设备ID、类别标签
BOOLEAN 布尔型 是否新用户、是否VIP
VECTOR 浮点向量 Embedding、图像特征
MAP KV结构 用户偏好、属性集合

你可能会问:为什么不用枚举?因为特征类型未来可能扩展,用字符串更灵活。我在项目中就踩过这个坑——一开始用 TINYINT 映射类型,后来新增 VECTOR 类型,所有客户端都得同步更新枚举映射表,太痛苦了。

status:特征的生命周期

状态机设计是注册表的核心。我把它分成四个阶段:

  • 草稿(0):刚录入,还在配置中,不可被服务发现。
  • 启用(1):正常服务中,可被在线和离线任务使用。
  • 灰度(3):只对部分流量生效,用于 A/B 测试。
  • 废弃(2):不再使用,但保留元数据供历史回溯。

这里有个细节:废弃不是删除。我曾经手欠删了一条废弃特征,结果三个月后要复现一个老模型,特征ID找不到了,只能从代码里反推……从那以后,我规定所有特征只能标记废弃,不能物理删除。

source_config:用 JSON 承载灵活性

不同特征的来源差异很大。Hive 特征需要表名和分区字段,Kafka 特征需要 topic 和解析逻辑,HTTP 特征需要 URL 和超时配置。如果每种来源都单独建字段,表会膨胀得没法看。JSON 字段正好解决这个问题——结构灵活,MySQL 8.0 以上还支持 JSON 索引和函数检索。

💡 小技巧:source_config 里建议加一个 version 子字段,记录数据源的版本号。比如 Hive 表结构变更时,你可以通过版本号快速判断哪些特征需要重新校准。

3.2 索引设计策略:查询快、写入稳、不冗余

表结构定好了,接下来是索引。注册表的写入量不大(一天几百条顶天了),但查询场景多——按ID查、按状态查、按负责人查、按更新时间查……索引设计要覆盖这些高频路径,又不能太多拖慢写入。

主键与唯一索引

  • PRIMARY KEY (id):自增主键,InnoDB 聚簇索引,没啥好说的。
  • UNIQUE KEY uk_feature_id (feature_id):业务唯一键。这是最重要的索引,几乎所有在线查询都走它。注意是唯一索引,不是普通索引——防止重复注册。

联合索引:状态 + 类型

KEY idx_status_type (status, feature_type) 是我最常用的联合索引。为什么?因为后台管理页面最常见的操作就是「查所有启用中的 INT 类型特征」或「查所有废弃特征」。把 status 放左边,是因为它区分度高(只有4个值),能快速过滤掉大部分数据。

负责人索引与时间索引

  • KEY idx_owner (owner):用于「我负责哪些特征」这类查询。权限管理和通知推送都会用到。
  • KEY idx_updated_at (updated_at):用于增量同步。离线任务每天拉取「今天有变更的特征列表」,走这个索引非常快。

📌 索引设计原则总结:

  • 在线查询(按 feature_id)走唯一索引,延迟控制在 1ms 以内。
  • 后台查询(按状态/类型/负责人)走联合索引或单列索引。
  • 增量同步(按更新时间)走时间索引,避免全表扫描。
  • 不要给 JSON 字段建太多虚拟列索引,除非你真的需要按 JSON 内部字段过滤。

3.3 注册表核心流程:一张图看懂

下面这张图展示了特征从注册到下线的完整生命周期,以及注册表在其中扮演的角色。

特征注册表核心流程 特征注册表核心流程 1. 特征注册 2. 写入 feature_registry 表 3. 状态流转(草稿→启用→废弃) 4. 下游消费(在线服务 / 离线任务) 5. 监控与治理(废弃清理 / 血缘追踪) 核心约束 • feature_id 全局唯一 • 废弃特征不可物理删除 • 类型变更需走审批

流程其实不复杂:注册 -> 入库 -> 状态流转 -> 被消费 -> 治理回收。但每一步都依赖注册表提供准确的元数据。你想想看,如果注册表里 feature_type 写错了,在线服务反序列化就会抛异常;如果 status 没及时更新,离线任务可能还在用已经废弃的特征……所以这张表虽然小,却是整个特征平台的「宪法」。

3.4 一些实战中的小建议

  • 字段命名统一用下划线风格:Java 端用 MapStruct 或 MyBatis 自动映射时,下划线转驼峰是标配,别搞特殊。
  • JSON 字段不要滥用:只有确实结构多变的配置才放 JSON。像 owner、status 这种固定字段,老老实实用独立列,查询效率高得多。
  • 预留扩展字段:我习惯加一个 ext_info JSON 字段,用于未来可能出现的临时需求。比如某天业务方说「我想给特征加个优先级」,直接往 ext_info 里塞,不用改表结构。
  • 定期巡检废弃特征:写一个定时任务,每周扫描 status=2 且 updated_at 超过 90 天的特征,推送给 owner 确认是否可归档。避免注册表无限膨胀。
🔧 扩展阅读:如果你用的是 TiDB 或其他分布式数据库,注意 feature_id 唯一索引的冲突处理。在高并发注册场景下,建议用 INSERT IGNORE 或先查后插,避免唯一键冲突导致事务回滚。