4、特征元数据管理:元数据包含哪些内容、元数据存储选型(MySQL vs Redis vs HBase)、元数据一致性保证

特征元数据管理,说白了就是给每个特征办一张「身份证」。我刚开始做特征平台时,觉得这步不就是存个名字和类型嘛,有啥好折腾的?结果上线第一天就出问题了——两个团队各自注册了同名特征,一个存的是用户年龄分段,另一个存的是用户注册天数,下游模型训练直接跑飞。嗯,从那以后我再也不敢小看元数据了。

4.1 元数据到底包含哪些内容?

一个完整的特征元数据,远不止「特征名 + 类型」这么简单。我个人习惯把它分成四个维度:

4.1.1 基础标识信息

  • 特征ID:全局唯一,通常用 UUID 或雪花算法生成。我见过直接用自增 ID 的,后来分库分表时改得想哭。
  • 特征名称:业务可读的名字,比如 user_purchase_30d_sum。
  • 特征类型:数值型、类别型、文本型、向量型等。
  • 特征分组:比如用户画像组、交易行为组、内容偏好组。
  • 版本号:每次变更必须递增版本,这是回溯的命根子。

4.1.2 存储与计算信息

  • 存储引擎:特征数据存在哪?Redis、HBase、MySQL 还是 ClickHouse?
  • 存储 Key 模板:比如 feature:user:{user_id}:purchase_30d。
  • TTL 策略:特征数据过期时间,避免冷数据占内存。
  • 计算逻辑:特征的 SQL 或代码片段,方便回溯和重算。
  • 依赖特征列表:这个特征依赖哪些上游特征?做级联更新时用得上。

4.1.3 血缘与治理信息

  • 创建人 / 负责人:出问题了找谁?
  • 创建时间 / 更新时间:审计用。
  • 数据源:来自哪张表、哪个 Kafka Topic、哪个实时流。
  • 质量规则:比如「值不能为 null」「数值范围 0-100」「枚举值必须在白名单内」。
  • 标签:比如「在线特征」「离线特征」「实验特征」「已废弃」。

4.1.4 访问控制信息

  • 可见范围:哪些团队可以读?哪些可以写?
  • 调用方白名单:防止下游乱接特征。
  • 限流阈值:QPS 上限,防止一个特征把整个集群打挂。

核心原则:元数据是特征的「说明书」,宁可多存不可少存。我踩过的坑是——当初没存计算逻辑,半年后特征数据异常,想重算比对,结果没人记得当初的 SQL 怎么写的了。

4.2 元数据存储选型:MySQL vs Redis vs HBase

这个问题我当年纠结了整整两周。你想想看,元数据既要支持频繁读写,又要保证一致性,还要能扩展。没有银弹,只有取舍。

维度 MySQL Redis HBase
数据模型 关系型,结构化强 KV + 部分数据结构 宽表,稀疏列族
一致性 强一致(ACID) 最终一致(主从) 行级强一致
读写性能 读 1-5ms,写 1-10ms 读 <1ms,写 <1ms 读 5-20ms,写 2-10ms
扩展性 分库分表,运维重 集群模式,水平扩展好 原生分布式,扩展性好
事务支持 支持 有限(Lua/Multi) 不支持跨行事务
适合场景 元数据管理后台、审计 高频读取、缓存 海量元数据、历史版本

4.2.1 我的推荐方案

说实话,没有哪个存储能包打天下。我在生产环境用的是分层存储:

  • MySQL 做主存储:存完整的元数据记录,支持事务和复杂查询。元数据管理后台、变更审计、血缘查询都走它。
  • Redis 做缓存:把高频读取的元数据(比如特征类型、存储 Key 模板)缓存到 Redis,QPS 能扛到 10 万+。
  • HBase 做历史归档:每次元数据变更都写一条 HBase 记录,保留全量历史版本,方便回溯和对比。

避坑指南:我曾经把元数据全量放 Redis,结果 Redis 宕机后整个特征平台不可用。后来改成 MySQL 兜底 + Redis 加速,Redis 挂了还能降级读 MySQL,虽然慢点但至少不瘫痪。

4.3 元数据一致性保证

元数据不一致,比数据不一致更可怕。数据不一致最多模型效果差,元数据不一致会导致整个平台逻辑错乱。我遇到过最离谱的一次——MySQL 里特征类型是「数值型」,Redis 缓存里是「类别型」,下游特征计算引擎直接报类型转换异常,排查了三个小时。

4.3.1 写操作的一致性

核心思路:先写 MySQL,再写 Redis,最后异步写 HBase。为什么?

  1. MySQL 支持事务,可以保证元数据变更的原子性。
  2. Redis 作为缓存,允许短暂不一致(秒级)。
  3. HBase 做历史归档,允许分钟级延迟。
// 伪代码:元数据更新流程
beginTransaction();
try {
    // 1. 写 MySQL
    metadataDAO.update(metadata);
    // 2. 写 Redis(设置过期时间,防止脏数据常驻)
    redisCache.setex(key, 300, metadata);
    // 3. 提交事务
    commit();
    // 4. 异步写 HBase 历史记录
    asyncExecutor.submit(() -> hbaseDAO.insertHistory(metadata));
} catch (Exception e) {
    rollback();
    // 补偿:删除 Redis 缓存
    redisCache.del(key);
    throw e;
}

4.3.2 读操作的一致性

读的时候,我习惯用缓存穿透保护 + 版本号校验:

  • 先读 Redis,拿到元数据和版本号。
  • 如果 Redis 没有,读 MySQL,同时回填 Redis。
  • 如果 Redis 有但版本号落后(比如 Redis 里的版本号是 3,MySQL 里是 5),说明缓存过期了,重新从 MySQL 加载。

注意:版本号校验能解决大部分不一致问题,但要注意并发场景。我遇到过两个线程同时更新同一个特征,版本号冲突导致缓存被旧数据覆盖。解决方案是:写 Redis 时用 SETNX 或 Lua 脚本做原子性版本检查。

4.3.3 最终一致性兜底

即使做了上面这些,还是可能出现不一致。怎么办?加一个定时对账任务:

  • 每 5 分钟扫描一次 MySQL 中的元数据变更记录。
  • 对比 Redis 中的版本号,发现不一致就强制刷新。
  • 记录对账日志,方便排查问题。

说白了,一致性保证不是靠一个方案搞定,而是「写时强一致 + 读时校验 + 定时兜底」三层防护。我做了三年特征平台,这套方案帮我挡住了至少 10 次线上事故。

4.4 元数据管理架构总览

下面这张图是我现在用的元数据管理架构,你可以参考一下:

特征元数据管理架构 元数据写入 API 事务管理器(ACID) MySQL(主存储) 强一致 · 事务 · 审计 Redis(缓存) 高频读 · 低延迟 · 秒级过期 HBase(历史归档) 全量版本 · 回溯 · 分钟级延迟 定时对账任务(5min 周期) 元数据读取 API 写时强一致(MySQL事务)→ 读时版本校验 → 定时对账兜底,三层保障元数据一致性

这张图的核心逻辑就是:写入走事务,缓存加速读,历史存 HBase,对账保一致。我每次给团队新人讲这套架构,都会强调一句话:「元数据是特征平台的命根子,怎么谨慎都不为过。」

个人经验:如果你团队小、特征数量在 1000 以内,直接用 MySQL 就够了,别整 Redis + HBase 那一套。我早期就是过度设计,结果运维成本比收益还高。先跑起来,再优化,这才是务实的态度。