Redis 在推荐系统中的角色:特征缓存、排序队列和去重集合
Redis 在推荐系统中的角色特征缓存、排序队列和去重集合一、三种角色一个 Redis推荐系统对数据结构的依赖超出预期Redis 在大多数后端架构里扮演的角色是缓存——存一下 Session放一下热点数据。但在推荐系统中Redis 的定位远不止缓存它是贯穿整个推荐链路的核心数据枢纽。推荐系统一天要读写的数据类型非常多样用户特征需要毫秒级查询Hash、推荐结果需要有序排列Sorted Set、用户曝光记录需要高效去重Set / HyperLogLog、实时特征需要原子累加String / Hash、候选池需要先进先出队列List。幸运的是这些需求恰好落在 Redis 的核心数据结构能力范围内。一个集群扛三种角色开发成本低、运维简单。但风险也在这里——所有鸡蛋在一个篮子里。如果 Redis 集群挂了推荐链路的三个环节同时停摆特征查不到、曝光去重失败重复推荐炸用户、排序队列丢失推荐顺序打乱。所以 Redis 在这套架构里不是可有可无的缓存而是强依赖的基础设施。基础设施不需要漂亮话它需要高可用方案和降级策略。二、特征缓存热数据的内存布局和过期策略用户特征是推荐系统里访问最频繁的数据。一个推荐请求可能涉及 1 个用户特征和 200 个物品特征每次特征查询如果都落到下游存储HBase/MySQL延迟和负载都扛不住。缓存命中率必须做到 95% 以上。用户特征用 Hash 存储Key 是user:{uid}:featuresField 是特征名Value 是特征值。这样做的好处是可以部分更新——用户点击了一个新品类只更新click_category这个 Field不用重写整个用户画像。物品特征中Embedding 向量是最特殊的一类。一个 128 维的 float32 向量 512 字节用 String 存储用 Protobuf 序列化后存。读取时直接反序列化避免 JSON 的解析开销。对于高频访问的 Top 10000 热门物品Embedding 需要在应用层再做一层本地缓存如 Go 的sync.Map或freecache把网络 IO 降为内存读取。过期策略上用户特征 TTL 设为 30 分钟——因为用户实时行为通过 Flink 写到 Redis 后30 分钟内必然会被推荐服务读取。超过 30 分钟没读到说明这个用户不活跃Key 自动删除腾内存。物品特征 TTL 更长设为 24 小时——物品特征变化慢但全量物品的 Embedding 向量更新一天一次就够了。内存预算控制是特征缓存的关键约束。一个 DAU 100 万的推荐系统活跃用户约 30 万每个用户特征 Hash 约 2KB总计 600MB。热门物品 10 万个每个 Embedding 512 字节总计 50MB。加上 Redis 自身开销约 1.5 倍总共约 1GB。再加上去重和排序的数据总内存预算控制在 4GB 以内。如果超了加机器不要调低 TTL——TTL 太短会导致缓存命中率下跌下游存储被打崩后果比加机器贵得多。三、曝光去重Set 的正确用法和 HyperLogLog 的精度取舍曝光去重是推荐系统里最容易被忽视但出了问题影响最大的一个环节。用户一天可能请求推荐上百次如果每次返回的内容里都混着重读的老面孔用户体验直接崩塌。短期去重用 Set。Key 是exposed:{uid}:{date}Value 是物品 ID 集合TTL 设 48 小时。每次精排前先 SMEMBERS 取出已曝光集合过滤掉这些物品后再排序。每次下发推荐结果后用 SADD 追加。但 Set 的问题是——如果用户一天看了 200 个物品Set 里就有 200 个元素没问题。但如果一个用户一天看了 5000 个物品重度短视频用户Set 的内存就会膨胀。这时候需要一种近似方案只保留最近 N 次曝光。Set 配合 LTRIM 的思想每次 SADD 后检查 Set 大小如果超过 500 个随机删除一些 Key——不是标准的 Set 操作但可以用 SPOP 随机弹出旧元素。长期去重用 Bitmap。Key 是exposed_bitmap:{uid}:{iid}按月分片。每个物品 ID 映射到一个 bit 位SetBit 标记已曝光GetBit 判重。Bitmap 比 Set 省内存——10 万个物品的去重只需要 12.5KB100000/8而 Set 里 500 个字符串型的物品 ID 就要用 20KB 左右。代价是物品 ID 和 bit 位置的映射需要额外维护一张表。UV 统计用 HyperLogLog。Key 是uv:{iid}每次曝光调用 PFADD。12KB 的空间可以统计数十亿 UV误差控制在 0.81%。如果需要分时间段统计用 BitMap 切分更合适——HyperLogLog 不支持取交集无法算同时看了视频 A 和 B 的用户数这是它的边界。四、Redis 承载力临界点什么时候该拆集群Redis 单集群扛三种角色在流量较小的阶段没什么问题。但当 QPS 突破 5 万开始出现三类信号说明该拆了信号一特征缓存的 SET 操作和去重集合的 SADD 操作CPU 时间被特征查询的 GET 操作抢占。特征查询是同步阻塞的推荐请求等它返回去重写入是异步的可以延迟几百毫秒。两者抢同一个 CPU后果是特征查询延迟抖动。信号二排序队列的 ZADD 操作数量暴涨。一个推荐请求写入 1 个 Sorted Set Entry10 万 QPS 就是每秒 10 万次 ZADD。如果和特征缓存的 HGET 放在同一个节点上磁盘 IO 和 CPU 都会成为瓶颈。信号三集群内存超过单节点上限。Redis Cluster 可以横向扩展但单个 Slot 的数据不能超过 64GB受限 Redis 的内存管理。一旦接近这个上限迁移 Slot 的时间会达到分钟级期间服务不可用。拆分的标准方案是按职责拆集群集群 A特征缓存 排序队列用 SSD Redis / RedisFlash 降低内存成本、集群 B去重集合用小内存高 IOPS 的 Redis 节点。两条链路在物理上隔离性能互不干扰。架构层面需要业务服务兼容多集群配置type RedisClients struct { FeatureCache *redis.ClusterClient // 特征缓存 排序 DedupSet *redis.ClusterClient // 去重集合 }五、总结Redis 在推荐系统中承担三种关键角色特征缓存的 Hash/String要求高命中率 低延迟、排序队列的 Sorted Set/List要求高吞吐写入、去重集合的 Set/Bitmap/HyperLogLog要求空间效率和近似精度。这三种角色的资源需求差异很大——特征缓存重延迟、排序队列重吞吐、去重集重空间。当单集群承载不了时按职责拆分是必然选择。拆分后业务侧用多客户端分别连接不同集群访问逻辑不变。Redis 在推荐架构里不是缓存而是强依赖基础设施。从部署第一天起就要配置 Sentinel 哨兵或 Cluster 模式确保主从切换在秒级内完成。特征查不到可以有兜底返回默认值去重失败可以让用户看到重复内容体验差但服务不挂排序队列丢失会自动重新生成——但 Redis 集群整体不可用超过 30 秒推荐服务会全面降级。这不是技术选择是可靠性底线。