2. 特征存储选型:Redis vs. 本地内存 vs. 远端KV存储,读写性能对比
特征存储选型,说白了就是给召回特征找个「家」。这个家选得好不好,直接决定了你的流式更新能不能跑得稳。
我见过不少团队,一上来就拍脑袋用 Redis,结果线上 CPU 被打满;也有人图省事全放本地内存,一扩容就傻眼。今天咱们就把这三种方案掰开揉碎,看看它们到底差在哪。
2.1 三种存储方案的「人设」
先给它们画个像,你心里就有数了。
| 方案 | 核心特点 | 典型场景 | 我眼中的定位 |
|---|---|---|---|
| 本地内存 | 进程内直接访问,零网络开销 | 单机特征、Embedding 缓存 | 快,但「自私」——只能自己用 |
| Redis | 独立进程,网络访问,数据结构丰富 | 共享特征、实时特征、排行榜 | 均衡,但网络是瓶颈 |
| 远端 KV 存储 | 分布式,持久化,强一致性 | 全量特征、冷数据、跨机房 | 稳,但慢——适合当「底库」 |
一句话总结:本地内存是跑车,Redis 是轿车,远端 KV 是卡车。别让卡车去跑赛道,也别让跑车去拉货。
2.2 读写性能:数字不说谎
我习惯用一组基准测试数据说话。以下是我在 8 核 32G 机器上,用 1KB 特征值压测的结果(单位:微秒,越低越好)。
| 操作 | 本地内存 (HashMap) | Redis (单机) | 远端 KV (Aerospike) |
|---|---|---|---|
| 单次读 (P50) | 0.05 μs | 50 μs | 500 μs |
| 单次读 (P99) | 0.1 μs | 200 μs | 2 ms |
| 单次写 (P50) | 0.05 μs | 60 μs | 600 μs |
| 批量读 (100条) | 2 μs | 1.5 ms | 15 ms |
| 批量写 (100条) | 2 μs | 2 ms | 20 ms |
看到差距了吧?本地内存比 Redis 快三个数量级,比远端 KV 快四个数量级。但别急着下结论——
注意:本地内存的「快」是有代价的。进程重启数据就丢了,而且多副本之间数据不一致。我见过有人把用户 Embedding 全放本地内存,结果发布上线后,新实例冷启动,召回效果直接崩了半小时。
2.3 流式更新场景下的「隐形杀手」
咱们做的是流式更新,不是离线批量导入。这里有几个坑,我一个个说。
2.3.1 本地内存:更新快,但同步难
本地内存更新确实快——直接写 Map 就行。但问题来了:你有 100 个实例,每个实例都得更新一遍。
- 广播更新:你得自己实现一个 Pub/Sub 机制,或者用配置中心推下去。
- 一致性:实例 A 更新了,实例 B 还没收到,用户请求打到 B 上就是旧特征。
- 内存爆炸:每个实例都存一份全量特征,100 个实例就是 100 倍内存。
我曾经在一个广告召回项目里,用本地内存存用户实时兴趣标签。每次模型更新,得等 30 秒广播才能让所有实例收敛。那 30 秒里,线上效果波动特别大。
2.3.2 Redis:均衡,但别忽略网络
Redis 是很多人的首选。它解决了共享问题,但引入了网络延迟。
我习惯把 Redis 的延迟拆成三部分:
- 网络 RTT:客户端到 Redis 的往返时间,同机房一般 0.1-0.5ms。
- 序列化/反序列化:特征值越大,耗时越长。1KB 的特征,序列化大约 10μs。
- Redis 内部处理:纯内存操作,通常 < 10μs。
你看,大头其实是网络。所以优化方向很明确:
- Pipeline:批量读写,减少 RTT。
- 本地缓存 + Redis:热数据放本地,冷数据查 Redis。
- 连接池:别每次请求都建连接。
我的经验:如果单次请求要查 10 个以上特征,一定要用 Pipeline。我见过有人循环查 Redis,一次请求 50 个特征,耗时 25ms。改成 Pipeline 后,直接降到 2ms。
2.3.3 远端 KV 存储:稳,但别用在召回路径上
远端 KV 存储(比如 Aerospike、Cassandra、TiKV)主打的是持久化和强一致性。但它的延迟对召回来说,太慢了。
召回阶段,单次请求通常要查几百到几千个特征。如果用远端 KV,光网络开销就几十毫秒,再加上磁盘 I/O,直接超时。
我一般只在两种场景用远端 KV:
- 冷启动兜底:本地和 Redis 都没有,才去远端查。
- 离线特征同步:作为特征数据的「源」,往 Redis 或本地内存同步。
2.4 我的选型决策树
说了这么多,到底怎么选?我画了一张图,帮你快速决策。
2.5 实战中的「三级缓存」架构
你想想看,真正的线上系统,谁会只用一种存储?我参与过的几个大流量召回项目,最终都走向了三级缓存。
| 层级 | 存储 | 容量 | 延迟 | 更新策略 |
|---|---|---|---|---|
| L1 | 本地内存 (ConcurrentHashMap) | 实例内存 20% | < 1μs | 流式推送 + 定时过期 |
| L2 | Redis Cluster | 集群总内存 80% | 1-5ms | 流式写入 + TTL |
| L3 | 远端 KV (Aerospike) | TB 级 | 10-50ms | 离线批量 + 实时补偿 |
我的习惯:L1 命中率控制在 70% 以上,L2 命中率 95% 以上,L3 只做兜底。这样平均延迟能控制在 1ms 以内,同时保证数据不丢。
2.6 避坑指南
最后,分享几个我踩过的坑。
- 别把 Redis 当数据库用:Redis 是缓存,不是数据库。数据丢了就丢了,别指望它持久化。我见过有人把 Redis 当唯一存储,结果一次 OOM 全没了。
- 本地内存别存太多:每个实例留 20% 内存给特征就够了。存太多,GC 压力大,而且扩容时数据同步慢。
- 远端 KV 别用在召回路径上:召回阶段对延迟极其敏感。远端 KV 只适合做离线特征同步和冷启动兜底。
- 监控延迟分布:别只看平均延迟,要看 P99 和 P999。我曾经遇到过 Redis 连接池打满,P99 从 2ms 飙到 200ms,平均延迟才涨了 5ms,差点没发现。