基于Redis+Kafka的首页曝光过滤方案

内容推荐系统曝光过滤
本文介绍了一种内容推荐系统的曝光过滤方案,通过客户端采集用户已曝光的内容,利用Kafka进行数据传输,Redis作为缓存存储曝光数据。采用SortedSets数据结构高效管理用户两周内的曝光记录。

本文来自网易云社区


作者:李勇


  • 背景

  网易美学首页除了banner和四个固定位,大部分都是通过算法推荐获取的内容,其中的内容包括心得、合辑、视频及问答等。现在需要实现的是当推荐内容在用户屏幕曝光后(即用户一个屏幕内的内容),那么这些内容在一定时间内,如两周内都不能再出现,因此需要对这些已经曝光过的内容进行过滤。首页内容如下图所示:



 20180903143644882b9803-1237-4065-bb8c-90d247dae2fd.jpg


  • 实现方案

  目前的实现方案是客户端对用户曝光的内容进行采集,然后通过DA的SDK将这些数据发送到Kafka Broker,然后再通过Kafka消费者去消费并解析这些数据,再将解析后的数据同步到Redis中。当用户再次获取数据时算法端会从Redis中获取需要过滤的数据,再将最终推荐内容返回服务端,然后服务端去业务数据库查询算法返回的数据对应的完整信息,最后将完整信息返回给客户端,客户端对数据进行渲染展现给用户。


  具体实现分为两道工序:一个是曝光数据的收集,另一个是对曝光数据的过滤。


  • 曝光数据的收集

  数据的收集步骤如下:

  1. 客户端收集用户的曝光内容;

  2. 客户端通过DA的SDK将收集到的用户曝光内容发送到Kafka集群;

  3. 实时计算工程实时拉取Kafka的内容;

  4. 提取出曝光内容再进行解析;

  5. 将解析后的内容以Sorted Set数据结构维护到Redis中。


  曝光数据的收集时序图如下图所示:

         201809031437263ddba141-f50a-4245-947e-39e6c4efb9a3.png


  • 曝光数据的过滤

  数据的过滤步骤如下:

  1. 用户使用客户端刷新首页数据;

  2. 客户端向服务端请求首页数据;

  3. 服务端在向算法端发送请求获取首页数据;

  4. 算法端根据服务端发送的用户个人信息,如用户id,用户设备id等信息,计算出推荐内容;

  5. 算法端从Redis中获取该用户最近两周的曝光数据进行过滤;

  6. 算法端将过滤后的推荐内容id和内容类型返回给服务端;

  7. 服务端根据算法返回的内容id和内容类型从数据库中查询详细信息并且组装数据;

  8. 服务端将最终的数据返回给客户端;

  9. 客户端受到服务端的数据将其展现给用户。


  曝光数据的过滤时序图如下图所示:

2018090314374743010af3-12cd-47d2-b3d5-fbf852fb4c43.png




  • 为何使用Kafka和Redis?

  Kafka是有LinkedIn开源的,是一个快速、可扩展的、高吞吐、可容错的分布式发布订阅消息系统,适合大规模消息处理场景。由于用户的曝光数据量比较大,因此使用Kafka作为消息系统。Kafka的优点有以下几方面:


  1. 高性能:每秒钟能够处理数以千计生产者生成的消息。

  2. 分布式:消息可以来自数以千计的服务,使用分布式处理这些数据。

  3. 持久性:Kafka会将数据持久化到硬盘上,避免数据丢失。

  4. 高扩展性:当容量不足时,Kafka可以简单的通过增加服务器进行横向扩展。


  Redis是一个高性能的key-value数据库,处理数据的效率高,考虑到首页获取数据的响应要求较高,因此使用Redis作为缓存数据库。Redis的优点有以下几方面:


  1. 性能极高:Redis的读取速度是11万次/s,写的速度是8.1万/s。

  2. 数据类型丰富:Redis相比其他key-value数据库,能够支持很多丰富的数据结构如Strings、Hashes、Lists、Sets和Sorted Sets。

  3. 原子性:Redis支持事务,能够对所有操作进行原子操作。

  4. 数据持久化:Redis支持数据的持久化,可以将内存中的数据保存到磁盘中,避免丢失。

  • 使用Redis的Sorted Sets

  Redis与其他key-value产品的一个不同点是它提供了丰富的数据结构类型,包括Strings、Hashes、Lists、Sets和Sorted Sets。用户曝光过的数据使用Soretd Sets数据结构进行维护,Sorted Sets与Sets类似之处是它不允许有重复的成员存在,不同的是每个元素都会关联一个double类型的分数。通过分数,Redis可以对集合中的成员进行从小到大的排序。Sorted Set的成员是唯一的,但是分数却是可以重复的,集合是通过哈希表实现的,因此,对Sorted Set进行添加、删除和查找时的时间复杂度都是O(1)。Sorted Sets中最大的成员数为2^32-1(4294967295,差不多40多亿),score是一个64位浮点类型,范围在-9007199254740992到9007199254740992之间。


    既然使用Sorted Sets数据结构作为用户曝光数据的载体,那么就需要确定Sorted Sets的key,score及value分别存储什么数据?由于key是唯一的,而每个用户的userId都是唯一的,因此可以用userId作为key来标识,但因为Redis不止一个工程用到,可能其他工程也共用该Redis集群,因此将使用“expose:userId”作为key。因为只保留用户最近两周的曝光数据,所以需要定时去删除两周前的数据,如果当前时间戳作为score,可以调用Redis的zremrangebyscore命令对两周前的数据进行清理。value结构保存用户的曝光具体数据,如内容的id,内容的类型,曝光的时间戳,手机的型号等信息,由于value是字符串类型,因此将这些数据转化成json格式封装。

  • 使用到的Sorted Sets相关命令

  Sorted Sets相关命令有20多种,包括了添加、删除、排序、查询、并集等命令,本方案中使用到了3种命令:


  1. zadd:zadd命令用于添加一个或多个成员元素及其分数到有序集合中,如果某一个成员已经在Sorted Sets中存在,那么更新这个成员的分数值,并通过重新插入该成员元素,来确保成员能够在正确的位置上。zadd命令的基本语法是zadd key_name score1 value1 score2 value2 … scoren valuen。具体命令如zadd 'expose:123456' 1505524199461 'This is value',表示将key为'expose:123456',score为1505524199461,value为'This is value'的数据添加到Redis中。201809031438165f60b2cf-232a-4b1d-96f6-84ff8e8d7676.png

  2. zremrangebyscore:zremrangebyscore命令用于移除Sorted Sets合中,指定分数区间内的成员。zremrangebyscore命令的基本语法是zremrangebyscore key_name min max,具体命令如:zremrangebyscore 'expose:123456' 1505524199461 1505634107000,表示将key为'expose:123456',并且1505524199461≤core≤1505634107000的数据删除。20180903143826bb3a67c1-7ef2-478c-98ae-805cd5946750.png

  3. zrevrangebyscore:zrevrangebyscore命令用于返回Sorted Sets中指定分数区间内的所有成员数据。并且数据是按照分数值递减,即从大到小的顺序排列,让分数相同时,成员的排序是根据字典序逆向排序。具体命令如:zrevrangebyscore 'expose:123456' 1505524199461 1505524199461,表示查询key为'expose:123456',并且1505524199461≤core≤1505524199461的所有数据,返回的数据根据score逆序排序。201809031438342ad341de-2801-454f-a2af-bb5dddaeea15.png


  • Sorted Sets的原理

  Sorted Sets内部是由字典(dict)和跳跃表(Skip List)来保证数据的存储有序。dict存放成员到score的映射,使用dict来实现的主要目的是为了保证查询复杂度为O(1),因为其内部是基于哈希表实现的。跳跃表则存放所有的成员,它的效率可以和平衡树相当,时间复杂度为O(logn),排序依据dict中的score,使用跳跃表的结构可以获得比较高的查询效率并且在实现上比较简单。


  Redis的数据结构如下所示:

//有序集数据结构typedef struct zset {
    dict *dict;//字典存放成员和score的映射
    zskiplist *zsl;
} zset;


  由上述结构可知,zset有两个成员:dict和zskiplist,其中的dict保存的是成员与score的映射,比如一个zset中存放了一个value为“abc”,score为1505524199000的数据,则dict中的key存放“abc”,value存放1505524199000。


  跳跃表由zskiplist和zskiplistNode组成,它们的数据结构如下所示:

//定义跳表的基本数据节点typedef struct zskiplistNode {
    robj *obj; // zset value
    double score;// zset score
    struct zskiplistNode *backward;//后向指针
    struct zskiplistLevel {//前向指针
        struct zskiplistNode *forward;        unsigned int span;
    } level[];
} zskiplistNode;//跳跃表结构typedef struct zskiplist {    struct zskiplistNode *header, *tail;    unsigned long length;    int level;
} zskiplist;


  可以将上述的结构转化成图形,如下图所示:

20180903143859c388e00f-5b23-4a7f-81fb-7a2b1593ab1d.png

  从上图可以看到header指针指向一个具有32层的表头节点,而定义成32层,因为理论上来说,2^32-1个元素的查询最优,并且2^32-1-1=4294967295,对于大部分应用来说这么多元素已经足够了。位于图片最左边的是zskiplist结构,该结构包含的属性有:


  1. header:指向跳跃表的表头节点。

  2. tail:指向跳跃表表尾的节点。

  3. level:记录目前跳跃表中,层数最大的那个节点的层次(不包括表头节点),如上图层数最大的那个节点是表尾节点,它有4层,因此level=4。

  4. length:跳跃表所包含的节点数(不包括表头节点),上图中不包括表头,右侧有3个节点,因此length=3。


  zskiplist右侧的四个元素为zskiplistNode结构,该机构包含以下属性:



  1. level(层):节点中L1、L2等元素,L1代表第一层,L2代表第二次,以此类推。每层都有两个属性,前进指针(forward)和跨度(span),前进指针用于向后访问其他节点,跨度则记录前进指针所执行节点与当前节点的距离,用于记录节点在跳跃表中的排名。

  2. backward(后退指针):节点中的BW表示后退指针,它指向当前节点的前一个节点,后退指针用于从表尾向表头遍历。

  3. score(分值):上图节点中的20.0、40.0、40.0代表各节点的分值,score是一个double类型的浮点数,跳跃表中的所有节点都按照score从小到大排序。

  4. obj(成员对象):上图节点中的Object1、Object2等代表节点所保存的成员对象,它是一个指针,指向一个字符串对象。


  跳跃表的遍历有个规则就是总是从高层开始往低层查找。例如当需要查找分值为40.0,成员对象为Object2时,查找的过程如下:


  1. 先从zskiplist中查找表头的位置;

  2. 再从表头的最上层查找,即先从L32开始查找,由于L32指向null,因此继续查找表头的L31,而L31依然指向null,依次往低层查找,直到找到L4,然后通过L4中的前进指针中到达Object3节点;

  3. 比较Object3节点的分数是否为40.0,再比较Object3对象是否为目标对象,继续往下层遍历;

  4. 通过表头的L3继续遍历,达到Object2,最后比较发现为目标对象,结束查找。

  • Sorted Sets的其他应用场景

  Sorted Sets的应用场景很多,下面介绍几种常见的场景:


  1. 关注列表和被关注者列表:可以设计两种队列,一种队列用于记录用户的关注列表,另一种用于记录关注者的列表。其中有序集合的成员为用户Id,而分值则记录了用户开始关注某人或者被某人关注的时间戳。key可以为队列标识符+当前用户id。

  2. 游戏排行榜:使用排行榜名称作为key,用户的游戏分数作为score,然后使用用户的id作为value,通过zrevrange命令(因为大部分排行榜都是按照分数由高到底排序,而有序集是从小到大排序的,因此需要使用zrevrange命令)可以很快查出用户的排名信息。


 

网易云大礼包:https://www.163yun.com/gift

本文来自网易云社区,经作者李勇授权发布。



相关文章:
【推荐】 Obfuscator-llvm源码分析

<think>我们正在讨论小红书用户标签功能的实现原理,特别是Redis的使用情况。用户标签系统是推荐系统的核心组成部分,它通过分析用户行为(如点赞、收藏、评论等)来构建用户兴趣模型,从而支持个性化推荐。下面我将结合行业通用方案和小红书可能的实现方式进行分析。 ### 1. **用户标签系统的核心流程** - **数据采集**:收集用户行为日志(如点击、停留时长、搜索词)。 - **标签计算**:通过NLP、机器学习模型(如Embedding)将行为转化为标签(例如:"美妆达人"、"旅行爱好者")。 - **存储与更新**:标签需实时或近实时更新,支持高并发读取。 - **应用层**:推荐服务根据标签召回内容(如"猜你喜欢")。 ### 2. **Redis在标签系统中的作用** Redis因其高性能(10万+ QPS)和丰富的数据结构,常被用于标签存储和实时更新。具体应用包括: - **存储用户画像**:使用Hash结构存储用户标签及权重,例如: ```redis HSET user:12345 tags "美妆:0.8, 穿搭:0.6" ``` - **实时计数**:用Sorted Set记录标签热度,例如: ```redis ZINCRBY hot_tags 1 "美妆" ``` - **去重与过滤**:用Set存储用户已曝光内容ID,避免重复推荐[^1]。 ### 3. **小红书可能的实现方案** - **标签生成**: - 短期兴趣:基于实时行为(如最近1小时点击)通过Flink计算,结果写入Redis。 - 长期兴趣:离线训练Embedding模型(如Word2Vec),将用户行为序列映射为向量,存入向量数据库(如Milvus)或Redis(通过Hash存储向量值)。 - **Redis使用场景**: - **缓存层**:将离线计算的全量标签(如每日更新)加载到Redis,供线上服务低延迟读取。 - **实时更新**:用户新行为触发增量标签更新,例如: ```redis HINCRBY user:12345 tag:美妆 0.1 # 增加"美妆"标签权重 ``` - **去重服务**:存储用户已读内容ID,例如: ```redis SADD user:12345:read_items 7890 ``` ### 4. **性能优化与挑战** - **内存优化**:采用压缩数据结构(如RedisZipList),或对标签ID化(用数字ID代替字符串)。 - **冷热分离**:高频访问的活跃用户标签存Redis,低频用户存数据库(如HBase)。 - **一致性保障**:通过Write-Ahead Log(WAL)或双写机制确保Redis与数据库同步。 ### 5. **替代方案对比** - **纯DB方案**(如MySQL/HBase):读写延迟高(>100ms),不适合实时推荐。 - **Redis+DB组合**:Redis抗并发读/写,DB持久化数据,是行业主流选择[^1]。 ### 总结 小红书用户标签系统**极大概率依赖Redis**,主要承担实时存储和缓存角色,其核心价值在于: - **低延迟响应**:标签读取<5ms,满足推荐系统实时性要求。 - **高并发支撑**:支持百万级用户同时更新标签。 若小红书未使用Redis,则需自研同等性能的存储组件,但成本与风险较高,因此Redis是合理选择。 --- **相关问题** 1. 用户标签权重如何通过机器学习模型动态更新? 2. Redis在标签系统中可能出现的数据一致性问题如何解决? 3. 除了Redis,还有哪些数据库适合存储用户画像? 4. 如何设计用户标签系统的容灾机制?</think>### 小红书用户标签功能实现原理及Redis应用分析 #### 1. **用户标签系统核心架构** 小红书用户标签系统采用分层处理架构,主要分为三个阶段: - **数据采集层**:实时捕获用户行为(点击/点赞/收藏/搜索),通过日志采集服务(如Flume/Kafka)传输到处理层[^1] - **标签计算层**: - 短期兴趣:实时计算引擎(如Flink)处理最近行为 - 长期兴趣:离线批处理(如Spark)分析历史行为 - 向量化表示:使用Embedding技术将用户行为转化为数学向量 $$ \vec{u} = (u_1, u_2, ..., u_n) $$ - **存储与服务层**:标签存储与实时查询接口 #### 2. **Redis的核心应用场景** Redis在小红书标签系统中扮演关键角色,主要用于: | **应用场景** | **数据结构** | **优势** | **示例** | |------------|------------|---------|---------| | **实时标签缓存** | Hash | 毫秒级读写延迟 | `HGET user:10086 interest_tags` | | **行为去重** | Set | 快速判断行为唯一性 | `SADD user:10086:viewed 7890` | | **热度计算** | ZSet | 自动排序热门标签 | `ZINCRBY hot_tags 1 "美妆"` | | **布隆过滤器** | Bitmap | 高效内容查重 | 检测相似笔记[^1] | | **会话存储** | String | 临时行为序列存储 | `SET session:ZXC987 "view=5432,like=8765"` | #### 3. **具体实现流程** 1. **行为采集**: ```python # 用户点赞事件示例 event = { "user_id": 10086, "action": "like", "item_id": 54321, "tags": ["美妆", "口红测评"], "timestamp": 1685432100 } # 通过Kafka发送到处理集群 kafka_producer.send("user_events", event) ``` 2. **标签计算**: - 实时计算(<1秒延迟): ```java // Flink实时处理伪代码 DataStream<UserEvent> stream = env.addSource(kafkaSource); stream.keyBy("user_id") .process(new TagCalculator()) // 更新兴趣权重 .addSink(redisSink); // 写入Redis ``` - 离线计算(每日更新): ```sql -- Hive离线计算 INSERT INTO user_tags SELECT user_id, COLLECT_SET(tag) AS tags, SUM(weight) AS score FROM user_behavior GROUP BY user_id ``` 3. **Redis存储结构**: ```redis # 用户标签存储 HMSET user:10086:tags "美妆" 0.85 "数码" 0.62 "旅行" 0.78 # 反向索引(标签->用户) SADD tag:美妆:users 10086 10010 20045 ``` #### 4. **性能优化策略** 1. **分层存储**: - Redis:存储热数据(最近7天活跃用户) - HBase/Cassandra:存储全量历史标签 2. **内存优化**: - 使用ziplist压缩Hash结构 - 标签ID化(用数字ID代替字符串) 3. **高可用设计**: - Redis Cluster分片部署 - 双写机制保障数据一致性 - 热Key检测 + 本地缓存 #### 5. **典型应用场景** 1. **个性化推荐**: ```python # 获取用户标签向量 user_tags = redis.hgetall("user:10086:tags") # 计算内容相似度 similarity = cosine_similarity(user_tags, video_tags) ``` 2. **去重控制**: ```java // 检查是否已推荐过 if (!redis.sismember("user:10086:recommended", videoId)) { recommendService.push(videoId); } ``` #### 6. **技术选型对比** | **组件** | **适用场景** | **小红书应用** | |---------|------------|--------------| | Redis | 实时读写/缓存 | ★★★★★ 核心存储 | | Elasticsearch | 标签检索 | ★★☆☆☆ 有限使用 | | HBase | 历史数据存储 | ★★★★☆ 离线存储 | | Flink | 实时计算 | ★★★★★ 主力计算引擎 | ### 总结 小红书用户标签系统**重度依赖Redis**,主要承担: 1. 实时标签缓存与更新 2. 去重控制和会话管理 3. 热度计算和快速检索 4. 推荐系统的实时响应支撑 其核心优势在于**微秒级读写能力**和**丰富的数据结构**,完美适配标签系统对低延迟和高并发的需求。实际应用中通常采用Redis Cluster分片架构,支撑亿级用户标签的实时更新。 --- **相关问题** 1. 如何设计用户标签系统的冷热数据分离架构? 2. Redis在标签系统中可能遇到哪些性能瓶颈?如何优化? 3. 用户兴趣衰减模型在标签系统中如何实现? 4. 小红书如何解决新用户的冷启动标签问题? 5. 用户隐私保护在标签系统中如何落地实施?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值