需求分析

接口分析:

App 有一个高频调用的首页推荐接口。

  1. 需要按热度顺序返回热门导师,课程,团队数据。
  2. 由于热点数据存在时效性,所以要考虑到时间因素,需要返回一定时间范围内的数据。
  3. 要支持页面向下滑动的时候加载新的内容,所以需要分页功能。

主要目的:

在多用户访问情况下,提高速度。并确保数据一定的时效性。

尝试1: 数据表反范式优化 + 内存缓存

课程内需要包括导师数据,团队内需要包括导师数据,导师内需要包括课程数据。
为了防止多次的联表 JOIN 查询,在课程内存储冗余的序列化的导师 ID 列表,团队和导师也是如此。
每次需要再通过 ID 去访问单独的导师,课程和团队。

使用了内存缓存 接口 URL 和对应的返回数据的键值对。缺点:因为这样储存无法知道,某次单条数据更新对应了哪个接口下的更新。所以每次数据更新,缓存都会全部清空。再次从数据库读取。

尝试2:Redis 有序集合

SortedSet: 主要存储有序集合,SortedSet的添加元素指令ZADD key score member [[score,member]…]会给每个添加的元素member绑定一个用于排序的值score,SortedSet就会根据score值的大小对元素进行排序,在这里就可以将 weight 当作score用于排序,SortedSet中的指令ZREVRANGE key start stop又可以返回指定区间内的成员,可以用来做分页。所以,SortedSet在这里是最适合的(插入,查询时间复杂度O(log(N))。

ZSet 实现原理

底层通过跳表实现。跳表通过多重索引,查找数据。log(n) 复杂度。插入时通过随机函数(产生一个随机数,50%概率再向上扩展,否则就结束。这样子,每一个元素能够有X层的概率为0.5^(X-1)次方。保证上层节点数大概是下层的1/2, 实现约等于 log(N)的复杂度),确定是否增加上级索引,以及索引的层数。

要用到的 ZSet 操作

zadd
语法:zadd key [NX|XX] [CH] [INCR] score value [score value…]
XX:
存在就更新已有数据,不存在添加数据

zunionstore
语法:zunionstore destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]
Redis Zunionstore 命令计算给定的一个或多个有序集的并集,其中给定 key 的数量必须以 numkeys 参数指定,并将该并集(结果集)储存到 destination 。
默认情况下,结果集中某个成员的分数值是所有给定集下该成员分数值之和 。

zrevrange
语法: zrevrange key start stop [WITHSCORES]
Redis Zrevrange 命令返回有序集中,指定区间内的成员。
其中成员的位置按分数值递减(从大到小)来排列。

zincrby
语法: zincrby key increment member
Redis Zincrby 命令对有序集合中指定成员的分数加上增量 increment

expire
语法: expire key time_in_seconds
Redis Expire 命令用于设置 key 的过期时间,key 过期后将不再可用。单位以秒计。

zrem
语法: zrem key member [member ...]
Redis Zrem 命令用于移除有序集中的一个或多个成员,不存在的成员将被忽略。

通过 Redis 有序集合自动更新内部的顺序,可以省去当缓存清空,再去数据库读取的情况。这样每次数据更新,只需要更新 Redis 内部单条数据就可以了,跳表更新顺序和索引复杂度 O(log N)。

实现思路

  1. 数据存储思路。使用 zadd,将日期作为 key, 点击量作为 score, 导师 id 作为 value。存储某一日期下热门数据。
  2. 获取一段时间内的热门数据时,通过 zunionstore 聚合该时间范围内 zset,分数为累加和。
  3. 数据增加删除通过 zadd, zrem。当每次点击量多一次时,更新通过 zincrby date 1 counselorId,把对应导师 id 下的点击量 +1。每次查询一段时间内的热门数据时,都执行一次 union 操作,就可以获取更新后的信息了。不去更新每一个热门范围结果集的原因是,假如结果集是基于一周,需要分别去更新前 7 天和后 7天的结果集,操作次数多,实现起来也不容易。
  4. 得到 zunoinstore 输出的结果集 destination 后,通过 zrevrange destination pageSize*(page - 1) pageSize*(page - 1) + pageSize 就可以返回按分数逆序排列的分页了。

可能的改进

  1. score 热度,可以通过权重计算算法进行计算。目的是找到影响某一结果因素的比重。如这种情况可以找到点赞数,点击量,收藏,评论和下单数之间的关系。通过统计计算权重。
  2. 前端本地缓存,访问历史数据不用每次调用接口。只有当用户下拉到新内容后,再重新获取。问题: 有可能新数据,和历史数据有重复,需要做去重处理。可以通过后端加一个全局 id 字段去重。(全局 id 可以存在 Redis 里,每次从 Redis 获取再更新)
  3. 个性化推荐。涉及机器学习算法,需要将多维度的用户数据输入,训练。可以通过增加用户日志记录的字段(如页面停留时间,访问不同接口频次)或通过建议用户丰富个人信息等增加用户信息维度作为输入,并且要记录不同用户访问过的页面日志,作为输出数据。累积到一定数据规模,进行训练。

I am a real pikachu!