4. Redis 高级特性:Pipeline、Lua 脚本、过期策略、持久化对特征存储的影响

Redis 在推荐系统里,说白了就是个「特征高速公路」。但路修好了,车怎么开、油怎么加、什么时候保养,这些才是真正决定性能的关键。这一章,我把自己踩过的坑和总结的经验都摊开来讲。

核心观点:Pipeline 解决网络开销,Lua 保证原子性,过期策略控制内存水位,持久化决定数据安全。四者缺一不可,但用错了全是坑。

4.1 Pipeline:批量操作的艺术

我在项目中遇到过最典型的场景:召回阶段需要同时读取用户最近 100 个行为特征。如果一条一条发命令,光网络往返就要 100 次 RTT。你想想看,100 次 × 1ms = 100ms,这还没算处理时间,接口就已经超时了。

Pipeline 的原理很简单——把多条命令打包,一次发送,一次接收。但要注意,它不是事务,中间某条命令失败不会影响其他命令执行。

// 错误示范:逐条读取
for (String key : userFeatureKeys) {
    String value = jedis.get(key);  // 100次网络往返
}

// 正确示范:Pipeline批量读取
Pipeline pipeline = jedis.pipelined();
for (String key : userFeatureKeys) {
    pipeline.get(key);
}
List<Object> results = pipeline.syncAndReturnAll();  // 1次网络往返

我的习惯:Pipeline 的批量大小控制在 50-100 条之间。太大会导致 Redis 输出缓冲区积压,反而变慢。我曾经试过一次塞 10000 条,结果 Redis 直接报错——输出缓冲区满了。

4.2 Lua 脚本:原子性的保证

特征存储里有个经典问题:读取特征 → 修改特征 → 写回特征。这三步如果分开做,并发场景下数据就乱了。Lua 脚本能解决这个问题,因为 Redis 执行 Lua 脚本是原子操作。

我举个实际例子。在实时特征计算中,我们需要维护用户的「最近 10 次点击商品 ID」列表。用 Lua 脚本来做,既保证原子性,又减少网络开销。

-- Lua脚本:维护最近N个点击记录
local key = KEYS[1]
local newItem = ARGV[1]
local maxLen = tonumber(ARGV[2])

-- 从左侧插入新记录
redis.call('LPUSH', key, newItem)
-- 裁剪列表长度
redis.call('LTRIM', key, 0, maxLen - 1)
-- 设置过期时间(单位:秒)
redis.call('EXPIRE', key, 86400)

return 1

避坑指南:我曾经在 Lua 脚本里写了循环,结果因为死循环把 Redis 主线程卡死了。记住,Lua 脚本里不要做耗时操作,默认 5 秒超时。如果脚本执行太久,所有其他命令都得排队等着。

4.3 过期策略:内存的守门员

特征数据是有时效性的。用户昨天的兴趣,今天可能就变了。如果所有特征都永久保存,内存迟早爆掉。Redis 提供了三种过期策略,我分别说说在特征存储里的应用。

策略 原理 特征存储场景 我的建议
定时删除 设置过期时间,到期立即删除 短期行为特征(如:最近1小时点击) 推荐使用,精度高
惰性删除 访问时检查是否过期,过期则删除 长期画像特征(如:用户性别) 配合定时删除使用
定期删除 每秒扫描10次,每次随机检查20个key 兜底策略,防止内存泄漏 必须开启,默认配置即可

嗯,这里要注意。我见过很多团队只设置了过期时间,但没考虑「过期 key 堆积」的问题。举个例子:你给每个用户特征设置了 24 小时过期,但每秒有 10 万个新特征写入。24 小时后,同时过期的 key 可能有 8.6 亿个。Redis 的定期删除根本处理不过来,内存就会持续上涨。

解决方案:给过期时间加一个随机偏移量。比如基础过期时间 86400 秒,再加一个 0-3600 秒的随机值。这样过期时间分散开,避免「雪崩式删除」。

4.4 持久化:数据安全的最后防线

特征数据丢了会怎样?用户画像重建、模型重新训练、推荐效果下降……我经历过一次 Redis 宕机,丢了 3 小时的用户实时特征,那感觉就像打仗时丢了地图。持久化就是你的备份地图。

Redis 提供了两种持久化方式,我整理了一个对比表:

特性 RDB(快照) AOF(追加文件)
数据完整性 可能丢失最后一次快照后的数据 最多丢失1秒数据(everysec模式)
恢复速度 快(直接加载到内存) 慢(需要重放所有命令)
文件大小 小(二进制压缩) 大(文本协议,可重写压缩)
对性能影响 fork子进程,可能卡顿 写入性能略降,可接受

我个人习惯是两者都开。RDB 做全量备份,AOF 做增量备份。但要注意配置别太激进。我曾经把 AOF 的 fsync 设成 always,结果写入性能直接掉了 50%。后来改成 everysec,既保证了最多丢 1 秒数据,性能影响也控制在 5% 以内。

特征存储的持久化建议:

  • 短期特征(过期时间 < 1小时):不需要持久化,丢了就丢了
  • 中期特征(过期时间 1小时-7天):开启 AOF everysec 模式
  • 长期特征(不过期或过期 > 7天):RDB + AOF 双开,定期备份到远端

4.5 综合架构:四者如何协同

光讲理论不行,我画了一张图,把 Pipeline、Lua、过期策略、持久化在特征存储中的协作关系展示出来。你看完应该能明白它们各自的位置。

Redis 高级特性在特征存储中的协作架构 特征写入请求 Pipeline:批量打包,减少网络开销 Lua 脚本:原子操作,保证数据一致性 Redis 核心:内存存储 + 命令执行 过期策略 定时删除 + 惰性删除 + 定期删除 持久化 RDB 快照 + AOF 追加文件 协作流程说明 特征写入 → Pipeline 批量打包 → Lua 保证原子性 → Redis 核心执行 → 过期策略控制内存 + 持久化保障安全

从图里你能看到,整个流程是串行的。Pipeline 在最前面把请求「压缩」了,Lua 在中间保证「不乱」,过期策略和持久化在底层做「后勤保障」。任何一个环节出问题,特征存储的稳定性都会受影响。

最后提醒一句:别把所有特征都塞进 Redis。我见过有人把全量用户画像(几亿条)都放 Redis,结果内存不够,频繁触发 OOM。特征存储要分层——热数据放 Redis,温数据放 SSD 缓存,冷数据放 HBase 或 ClickHouse。这个分层策略,后面章节我会详细讲。