本文来自网易云社区

作者:李勇

  • 背景

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

  • 实现方案

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

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

  • 曝光数据的收集

数据的收集步骤如下:

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

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

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

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

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

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

  • 曝光数据的过滤

数据的过滤步骤如下:

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

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

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

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

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

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

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

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

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

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

  • 为何使用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中。

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

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

  • 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;

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

从上图可以看到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

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

相关文章:
【推荐】 认识用户访谈

基于Redis+Kafka的首页曝光过滤方案的更多相关文章

  1. 基于Redis分布式锁的正确打开方式

    分布式锁是在分布式环境下(多个JVM进程)控制多个客户端对某一资源的同步访问的一种实现,与之相对应的是线程锁,线程锁控制的是同一个JVM进程内多个线程之间的同步.分布式锁的一般实现方法是在应用服务器之 ...

  2. 基于redis的分布式锁实现方案--redisson

    实例代码地址,请前往:https://gitee.com/GuoqingLee/distributed-seckill redis官方文档地址,请前往:http://www.redis.cn/topi ...

  3. 基于redis 实现分布式锁的方案

    在电商项目中,经常有秒杀这样的活动促销,在并发访问下,很容易出现上述问题.如果在库存操作上,加锁就可以避免库存卖超的问题.分布式锁使分布式系统之间同步访问共享资源的一种方式 基于redis实现分布式锁 ...

  4. 基于redis的订单号生成方案

    目前,比较火的nosql数据库,如MongoDB,Redis,Riak都提供了类似incr原子行操作. 下面是PHP版的一种实现方式: <?php /** * 基于Redis的全局订单号id * ...

  5. 基于Flume+Kafka+ Elasticsearch+Storm的海量日志实时分析平台(转)

    0背景介绍 随着机器个数的增加.各种服务.各种组件的扩容.开发人员的递增,日志的运维问题是日渐尖锐.通常,日志都是存储在服务运行的本地机器上,使用脚本来管理,一般非压缩日志保留最近三天,压缩保留最近1 ...

  6. 【一个idea】YesSql,一种在经典nosql数据库redis上实现SQL引擎的方案(我就要开历史的倒车)

    公众号链接 最高级的红酒,一定要掺上雪碧才好喝. 基于这样的品味,我设计出了一套在经典nosql数据库redis上实现SQL引擎的方法.既然redis号称nosql,而我偏要把SQL加到redis上, ...

  7. Tomcat7基于Redis的Session共享实战二

    目前,为了使web能适应大规模的访问,需要实现应用的集群部署.集群最有效的方案就是负载均衡,而实现负载均衡用户每一个请求都有可能被分配到不固定的服务器上,这样我们首先要解决session的统一来保证无 ...

  8. 基于redis实现可靠的分布式锁

    什么是锁 今天要谈的是如何在分布式环境下实现一个全局锁,在开始之前先说说非分布式下的锁: 单机 – 单进程程序使用互斥锁mutex,解决多个线程之间的同步问题 单机 – 多进程程序使用信号量sem,解 ...

  9. 基于redis排行榜的实战总结

    前言: 之前写过排行榜的设计和实现, 不同需求其背后的架构和设计模型也不一样. 平台差异, 有的立足于游戏平台, 为多个应用提供服务, 有的仅限于单个游戏.排名范围差异, 有的面向全局排名, 有的只做 ...

随机推荐

  1. Python 输出中文的笔记

    import sysreload(sys)sys.setdefaultencoding('utf8') 导入csv乱码: 加入: import codecs csvfile.write(codecs. ...

  2. 图片背景2X && 3X

    图片背景2X && 3X @media (-webkit-min-device-pixel-ratio: 3),(min-device-pixel-ratio: 3){ .share_ ...

  3. Google Fonts导致网页加载速度慢

    最近在做商城项目时候发现在加载一个html页面反应非常慢,查看发现是Google Font导致的网页加载速度缓慢,删除掉该样式会发现很多内容出错. 上网百度发现问题在于: 谷歌香港(google.co ...

  4. Python之基本排序算法的实现

    import cProfile import random class SortAlgorithm: def __init__(self,unsortedlist=[]): self.unsorted ...

  5. etcd部署说明

    etcd是一个K/V分布式存储,每个节点都保存完成的一份数据.有点类似redis.但是etcd不是数据库. 1.先说废话.之所以会用etcd,并不是实际项目需要,而是前面自己写的上传的DBCacheS ...

  6. Unity 游戏框架搭建 (二十一) 使用对象池时的一些细节

    上篇文章使用SafeObjectPool实现了一个简单的Msg类.代码如下: class Msg : IPoolAble,IPoolType { #region IPoolAble 实现 public ...

  7. 在SQL Server中批量修改有规律列的定义

    )=N'要修改的表名'; --修改所有以sl结尾的列名的小数位数为4位 select syscolumns.name into #t1 from syscolumns,systypes where s ...

  8. javascript 时间倒计时效果

    <div id="divdown1"></div> <script language="javascript" type=&quo ...

  9. 史上更全的 MySQL 高性能优化实战总结!

    1 前言 2 优化的哲学 3 优化思路 3.1 优化什么 3.2 优化的范围有哪些 3.3 优化维度 4 优化工具有啥? 4.1 数据库层面 4.2 数据库层面问题解决思路 4.3 系统层面 4.4 ...

  10. python爬虫学习笔记(1)

    一.请求一个网页内容打印 爬取某个网页: from urllib import request # 需要爬取的网页 url = "https://mbd.baidu.com/newspage ...