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。为什么?
- MySQL 支持事务,可以保证元数据变更的原子性。
- Redis 作为缓存,允许短暂不一致(秒级)。
- 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 元数据管理架构总览
下面这张图是我现在用的元数据管理架构,你可以参考一下:
这张图的核心逻辑就是:写入走事务,缓存加速读,历史存 HBase,对账保一致。我每次给团队新人讲这套架构,都会强调一句话:「元数据是特征平台的命根子,怎么谨慎都不为过。」
个人经验:如果你团队小、特征数量在 1000 以内,直接用 MySQL 就够了,别整 Redis + HBase 那一套。我早期就是过度设计,结果运维成本比收益还高。先跑起来,再优化,这才是务实的态度。