特征存储(Feature Store)是连接机器学习离线训练与在线推理的关键组件。很多人把精力放在模型结构调优上,却忽略了特征供给这一环节——线上推理时特征取不到、取出来的值和训练时对不上、高并发下特征服务响应超时,这些问题都会让一个训练指标漂亮的模型在线上表现拉胯。Redis作为内存数据库,读写延迟稳定在亚毫秒级,配合TensorFlow的推理链路,可以很好地解决在线特征的供给问题。这篇文章围绕Redis与TensorFlow的配合使用,从架构设计到代码实现完整讲一遍。

一、为什么在线特征存储首选Redis
先明确特征存储的两个场景。离线场景下,特征通常存放在数据仓库或Parquet文件中,供训练任务批量读取,对延迟不敏感;在线场景下,推理请求到来时必须在几毫秒到几十毫秒内把特征拼齐,这对存储的延迟要求极为苛刻。
关系型数据库在这个场景下基本不可用。一次MySQL查询走索引也要毫秒级的响应,再加上网络往返,单条请求获取十几个特征可能就要几十毫秒,整个推理链路的延迟预算会被吃掉大半。Redis的所有数据常驻内存,单实例的读写延迟通常在0.1毫秒左右,而且支持Pipeline批量操作,一次网络往返就能取回上百个特征字段,这是磁盘型数据库无法比拟的。
除了性能,Redis的数据结构也很契合特征存储的需求。Hash类型天然适合存储一个样本的全部特征,一个key对应一个用户或一条商品,field则对应各个特征名,读写单个特征不需要反序列化整个对象。String类型配合JSON或Protobuf序列化则适合存储Embedding向量这类整体读写的特征。
二、特征数据结构设计与写入实现
设计key时建议遵循业务域:实体ID:特征版本的格式,例如user:10086:v2。带上版本号非常重要,因为特征口径会随业务迭代变化,版本隔离可以避免新旧模型读到不一致的特征定义。
标量类特征用Hash存储最合适,数值型、分类型特征都可以作为field写入。向量类特征(如用户Embedding)因为总是整体读取,用String存储序列化后的字节更高效。下面是Python写入示例:
import redis
import numpy as np
pool = redis.ConnectionPool(host='127.0.0.1', port=6379, max_connections=50)
r = redis.Redis(connection_pool=pool)
def write_scalar_features(user_id, features: dict):
"""标量特征用Hash存储,设置过期时间避免脏数据堆积"""
key = f"user:{user_id}:v2"
r.hset(key, mapping=features)
r.expire(key, 86400)
def write_embedding(user_id, vec: np.ndarray):
"""向量特征序列化后用String存储"""
key = f"user_emb:{user_id}:v2"
r.set(key, vec.tobytes(), ex=86400)
# 批量写入示例:Pipeline减少网络往返
pipe = r.pipeline(transaction=False)
for uid in [1001, 1002, 1003]:
pipe.hset(f"user:{uid}:v2", mapping={"age": 25, "city_level": 2})
pipe.execute()
注意两点细节。第一,Hash的field只支持字符串,数值写入后要注意读取端的一致转换,建议统一在写入侧完成类型标注,例如把特征名写成age:float这种带类型后缀的形式。第二,一定要设置TTL,过期时间可以根据特征更新周期设定,通常设为更新周期的两到三倍,既保证数据新鲜度,又能在上游写入故障时自动清理失效数据。
三、TensorFlow侧读取特征与服务对接
推理服务通常有两种形态。第一种是模型服务化部署,用TF Serving或TensorFlow Serving自定义op读取特征,这种情况下特征获取逻辑在服务外层,用Python或Go写一个特征聚合层更灵活;第二种是模型内部直接读取,通过TF的IO操作连接Redis,这种方案耦合度高,一般不推荐。
更常见的做法是:特征聚合服务从Redis取特征,拼装成Tensor再喂给模型。示例代码如下:
import numpy as np
import tensorflow as tf
def fetch_features(user_ids):
"""批量获取用户特征,返回可直接喂给模型的Tensor"""
pipe = r.pipeline(transaction=False)
for uid in user_ids:
pipe.hgetall(f"user:{uid}:v2")
rows = pipe.execute()
features = {"age": [], "city_level": []}
for row in rows:
features["age"].append(float(row.get("age", 0)))
features["city_level"].append(int(row.get("city_level", 0)))
return {k: tf.constant(v, dtype=tf.float32) for k, v in features.items()}
# 加载SavedModel并执行推理
model = tf.saved_model.load("exported_model")
infer = model.signatures["serving_default"]
output = infer(age=..., city_level=...)
这里有一个必须严肃对待的问题:训练与推理的特征一致性。线上最常见的坑是训练时特征做了归一化或缺失值填充,而在线读取时忘了做同样的处理。推荐的解决方案是把特征变换逻辑也沉淀下来,要么用TensorFlow Transform在训练和推理时执行同一套计算图,要么把变换参数(如均值、方差、分桶边界)也存入Redis,在线读取后先应用变换再进模型。缺失值的处理策略要在线上代码中显式实现,row.get("age", 0)里的默认值必须与训练时的填充值完全一致。
四、高并发下的性能优化与稳定性保障
当推理QPS上来之后,特征服务的压力主要在两个方面:Redis的连接数和热点key。连接数问题用连接池加合理的max_connections配置解决,一般单个推理实例开10到20个连接就够了,实例数量多时要注意Redis服务端的maxclients上限。
热点key指的是少数热门商品或头部用户的特征被高频访问。Redis单线程处理命令,某个key的访问集中度过高时会形成瓶颈。可以采取读写分离(从副本读非强一致特征)或对热点特征在前置层做本地缓存,比如用进程内LRU缓存热点实体,命中率能到80%以上时对Redis的压力会显著下降。
缓存穿透也需要防护。当请求的用户ID在Redis中不存在时,hgetall返回空字典,如果直接走默认值逻辑,恶意刷不存在的ID会把大量请求打到下游数据库。建议对不存在的key写入一个空值的哨兵标记并设置较短TTL,或者在特征聚合层对ID做合法性预校验。
五、监控指标与日常运维
特征服务上线后,有三个指标必须持续监控:特征命中率、读取延迟P99、特征新鲜度。命中率低于预期说明上游写入任务可能有延迟;读取延迟突然上升通常是热点key或网络问题;特征新鲜度指key距离最后更新的时间,如果超出更新周期,说明调度任务出了故障。可以写一个定时脚本扫描ttl值,TTL低于阈值时触发告警。
另外建议定期做特征快照对账:从线上流量中抽样请求,把当时的Redis特征值落盘,与离线仓库中的特征对比,一致性偏差超过一定比例就要排查口径问题。这个对账机制能提前发现大量线上效果异常的根因,投入产出比很高。整体来看,Redis加TensorFlow的组合并不复杂,难的是把特征口径管理、监控对账这些工程规范建立起来,这些才是特征存储体系长期稳定运行的根基。
Redis特征存储TensorFlow模型服务机器学习特征平台修改时间:2026-09-10 00:40:45