作者:李勇
网易美学首页除了banner和四个固定位,大部分都是通过算法推荐获取的内容,其中的内容包括心得、合辑、视频及问答等。现在需要实现的是当推荐内容在用户屏幕曝光后(即用户一个屏幕内的内容),那么这些内容在一定时间内,如两周内都不能再出现,因此需要对这些已经曝光过的内容进行过滤。首页内容如下图所示:
目前的实现方案是客户端对用户曝光的内容进行采集,然后通过DA的SDK将这些数据发送到Kafka Broker,然后再通过Kafka消费者去消费并解析这些数据,再将解析后的数据同步到Redis中。当用户再次获取数据时算法端会从Redis中获取需要过滤的数据,再将最终推荐内容返回服务端,然后服务端去业务数据库查询算法返回的数据对应的完整信息,最后将完整信息返回给客户端,客户端对数据进行渲染展现给用户。
具体实现分为两道工序:一个是曝光数据的收集,另一个是对曝光数据的过滤。
数据的收集步骤如下:
曝光数据的收集时序图如下图所示:
数据的过滤步骤如下:
曝光数据的过滤时序图如下图所示:
Kafka是有LinkedIn开源的,是一个快速、可扩展的、高吞吐、可容错的分布式发布订阅消息系统,适合大规模消息处理场景。由于用户的曝光数据量比较大,因此使用Kafka作为消息系统。Kafka的优点有以下几方面:
Redis是一个高性能的key-value数据库,处理数据的效率高,考虑到首页获取数据的响应要求较高,因此使用Redis作为缓存数据库。Redis的优点有以下几方面:
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相关命令有20多种,包括了添加、删除、排序、查询、并集等命令,本方案中使用到了3种命令:
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;
可以将上述的结构转化成图形,如下图所示:
从上图可以看到header指针指向一个具有32层的表头节点,而定义成32层,因为理论上来说,2^32-1个元素的查询最优,并且2^32-1-1=4294967295,对于大部分应用来说这么多元素已经足够了。位于图片最左边的是zskiplist结构,该结构包含的属性有:
zskiplist右侧的四个元素为zskiplistNode结构,该机构包含以下属性:
跳跃表的遍历有个规则就是总是从高层开始往低层查找。例如当需要查找分值为40.0,成员对象为Object2时,查找的过程如下:
Sorted Sets的应用场景很多,下面介绍几种常见的场景:
网易云大礼包:https://www.163yun.com/gift
本文来自网易实践者社区,经作者李勇授权发布。