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、过期策略、持久化在特征存储中的协作关系展示出来。你看完应该能明白它们各自的位置。
从图里你能看到,整个流程是串行的。Pipeline 在最前面把请求「压缩」了,Lua 在中间保证「不乱」,过期策略和持久化在底层做「后勤保障」。任何一个环节出问题,特征存储的稳定性都会受影响。
最后提醒一句:别把所有特征都塞进 Redis。我见过有人把全量用户画像(几亿条)都放 Redis,结果内存不够,频繁触发 OOM。特征存储要分层——热数据放 Redis,温数据放 SSD 缓存,冷数据放 HBase 或 ClickHouse。这个分层策略,后面章节我会详细讲。