3. 特征工程加速:在线特征计算优化、特征存储(Redis)性能调优、特征预计算与缓存策略

好,咱们进入第三个实战章节。特征工程这块,说白了就是推荐系统的「燃料生产线」。你模型再牛,特征算得慢、存得乱,线上照样卡成PPT。我见过太多团队,模型AUC刷得飞起,一上线延迟直接飙到500ms,最后发现是特征计算拖了后腿。

这一章,咱们就聚焦三个核心痛点:在线特征计算怎么省时间Redis怎么扛住高并发预计算和缓存怎么设计才不翻车。嗯,都是我在生产环境里踩过的坑,咱们一个一个拆。

3.1 在线特征计算优化:别让CPU烧在重复劳动上

在线特征计算,说白了就是用户请求来了,你现场算特征。比如用户的历史点击率、物品的实时热度、上下文的时间特征。这些计算如果没优化,每次请求都重复算一遍,CPU直接冒烟。

我个人习惯,先把计算链路画出来。你会发现,很多计算是「重复轮子」——比如用户画像里的「近7天点击率」,每次请求都从原始日志里聚合一遍,这不是傻吗?

核心优化原则:能预计算的就别在线算,能缓存的就别重复查,能异步的就别阻塞主流程。

3.1.1 计算拆分:实时 vs 准实时 vs 离线

我建议你把特征计算拆成三个层级:

  • 离线计算:用户长期偏好、物品基础属性。每天凌晨跑一次,写入Redis或HBase。
  • 准实时计算:近1小时的行为统计、滑动窗口特征。用Flink或Spark Streaming,分钟级更新。
  • 实时计算:当前请求的上下文(时间、设备、位置)。这个必须在线算,但可以极致优化。

举个例子。我在项目中遇到过,有个团队把「用户近30天购买金额」放在线算,每次请求都去扫描用户行为表。后来改成离线预计算,每天更新一次,延迟从200ms降到了5ms。你想想看,这中间差了多少倍?

3.1.2 特征计算函数的内联与缓存

在线计算时,很多特征函数是重复调用的。比如计算「当前时间是否在高峰期」,每次请求都new一个Calendar对象,这开销不小。

我常用的优化手段:

  • 函数内联:把简单的特征计算逻辑直接写在主流程里,避免函数调用开销。
  • 局部缓存:同一个请求内,多次用到的中间结果(比如用户基础画像)只查一次。
  • 对象池:频繁创建的对象(比如时间格式化器)用池化技术复用。
// 优化前:每次请求都创建SimpleDateFormat
public String getTimeFeature(long timestamp) {
    SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");
    return sdf.format(new Date(timestamp));
}

// 优化后:使用ThreadLocal缓存
private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT = 
    ThreadLocal.withInitial(() -> new SimpleDateFormat("HH:mm"));

public String getTimeFeature(long timestamp) {
    return DATE_FORMAT.get().format(new Date(timestamp));
}

你看,就改了一行,在高并发下能省下大量GC压力。我曾经在一个日活千万的系统里,靠这种小优化把CPU使用率降了15%。

3.2 特征存储(Redis)性能调优:别让缓存成为瓶颈

Redis是特征存储的标配,但用不好就是灾难。我见过最夸张的案例,Redis集群QPS才2万就扛不住了,一查发现全是慢查询和bigkey。

为什么会这样?说白了,很多人把Redis当成了「万能存储」,什么特征都往里塞,也不管数据结构合不合理。

3.2.1 数据结构选型:别只会用String

Redis有丰富的数据结构,选对了性能翻倍:

特征类型 推荐结构 原因
用户标签(多值) Set 支持交集、并集,适合人群圈选
用户行为序列 List 支持左推右弹,适合滑动窗口
物品实时热度 Sorted Set 按分数排序,适合TopK查询
用户画像(多字段) Hash 节省内存,支持部分更新

嗯,这里要注意。很多人喜欢把所有特征拼成一个大的JSON字符串存成String。这样做查询方便,但更新一个字段就得整个重写,而且内存浪费严重。我建议用Hash,每个字段独立存储,更新时只改一个field。

3.2.2 避免bigkey和hotkey

这是Redis性能的两大杀手。

  • Bigkey:单个key的value过大(比如一个用户的特征列表有几万个元素)。会导致读写耗时剧增,甚至阻塞其他请求。
  • Hotkey:某个key被超高频率访问(比如热门物品的特征)。会导致单台Redis节点CPU打满。

避坑指南:我曾经在生产环境遇到过,一个「热门视频」的Sorted Set里有50万个元素,每次查询Top100都要扫描整个集合,导致Redis CPU 100%。后来拆成「小时级」和「天级」两个小集合,问题解决。

解决方案:

  • Bigkey:拆分存储,比如用户行为序列按时间分桶。
  • Hotkey:本地缓存+Redis二级缓存,或者用一致性哈希打散到多个节点。

3.2.3 连接池与管道优化

高并发下,每次请求都创建Redis连接是找死。必须用连接池。

我常用的配置:

# Jedis连接池配置示例
jedis.pool.maxTotal=200        # 最大连接数
jedis.pool.maxIdle=50          # 最大空闲连接
jedis.pool.minIdle=10          # 最小空闲连接
jedis.pool.maxWaitMillis=3000  # 获取连接超时时间

另外,批量获取特征时,用Pipeline代替多次round-trip。比如一次请求需要查10个用户特征,用Pipeline一次发送,网络开销从10次降到1次。

3.3 特征预计算与缓存策略:空间换时间的老手艺

预计算,说白了就是「把计算提前做好,存起来,线上直接读」。这个思路不新鲜,但怎么设计缓存策略,才是真功夫。

3.3.1 预计算策略:全量 vs 增量

我建议分场景选择:

  • 全量预计算:适合变化慢的特征(用户性别、物品类目)。每天凌晨跑一次,全量覆盖。
  • 增量预计算:适合实时性要求高的特征(用户近1小时点击)。用Flink做滑动窗口,每5分钟更新一次。

举个例子。用户「近7天点击的类目分布」,如果全量算,每天凌晨跑一次就够了。但「近1小时点击的类目分布」,必须用增量方式,每来一条新行为就更新一次。

3.3.2 缓存层级:本地缓存 + 分布式缓存

我常用的缓存架构是两层:

  1. 本地缓存(Caffeine/Guava):放在应用进程内,访问速度纳秒级。适合读多写少、一致性要求不高的特征。
  2. 分布式缓存(Redis):作为共享存储,所有实例共享。适合需要全局一致的特征。

查询流程:先查本地缓存,命中直接返回;没命中再查Redis,回填本地缓存。

小技巧:本地缓存的过期时间要短于Redis,比如本地设30秒,Redis设5分钟。这样既能保证时效性,又能减少Redis压力。

3.3.3 缓存更新策略:别让数据「脏」了

缓存更新有几种常见模式:

  • Cache-Aside:读时回填,写时更新数据库并删除缓存。最常用,但要注意并发写导致的缓存不一致。
  • Write-Through:写操作同时更新数据库和缓存。适合一致性要求高的场景。
  • 异步刷新:后台线程定期拉取最新数据,更新缓存。适合容忍短暂不一致的特征。

我个人习惯用Cache-Aside + 异步刷新。线上请求先读缓存,如果缓存过期或不存在,回源查数据库并回填。同时,后台有个定时任务,每5分钟刷新一次热点特征的缓存,避免突发流量打穿。

3.3.4 缓存穿透、击穿、雪崩的应对

这三个问题,做推荐系统的应该都听过。我简单说下实战解法:

问题 现象 解法
缓存穿透 查询一个不存在的key,每次都穿透到DB 布隆过滤器 + 缓存空值(设置短过期时间)
缓存击穿 热点key过期,大量请求同时打到DB 互斥锁(只让一个线程回源) + 热点key永不过期
缓存雪崩 大量key同时过期,DB被打爆 过期时间加随机值 + 本地缓存兜底

嗯,这里要特别提一下。我曾经在双十一大促时,就因为缓存雪崩导致推荐服务挂了5分钟。后来学乖了,所有特征的过期时间都加一个±30%的随机偏移,再也没出过问题。

3.4 实战案例:一个特征加速的完整链路

最后,我分享一个实际项目的优化案例。当时我们做短视频推荐,特征计算是整个链路的瓶颈。

优化前:

  • 用户特征:每次请求从MySQL查用户行为表,实时聚合近7天数据。
  • 物品特征:从HBase查物品属性,每次都要反序列化整个对象。
  • 缓存:只用了一层Redis,热点key经常被打爆。

优化后:

  1. 用户特征:离线预计算写入Redis Hash,在线只读。
  2. 物品特征:拆分成多个小Hash,按类别分桶,避免bigkey。
  3. 缓存架构:本地Caffeine缓存热点特征,Redis作为二级缓存。
  4. 计算优化:用Pipeline批量获取特征,减少网络开销。

结果:P99延迟从350ms降到了45ms,Redis QPS从8万降到了1.2万(因为本地缓存扛住了大部分请求)。

你看,特征工程加速,说白了就是「算得少、存得好、读得快」。把这三件事做到位,你的推荐系统性能至少提升一个数量级。下一章,咱们聊聊模型推理的加速,那又是另一番天地了。