基于redis的多维度排序

概览

为什么需要多维度排序

常见的feed流架构下,我们需要很多的排序流,比如 雪球的评论流,需要按赞和时间进行排序(有赞的评论,先按赞排序。没有赞的评论按时间倒排)。

需求:按赞和评论排序。QPS > 5000,需要缓存。

常见设计方案:

  1. feed流里面存储按时间排序feed流。定时或者异步计算时间因子。但是这个时候会面临一个问题,你的feed流永远计算不是实时的。你的热贴流永远计算不及时。所以方案一不可取。
  2. 采用bit方式, 统一或若干小domain(比如点赞数), 再在排序后面接bit控制。核心在于如何计算score。

版本

redis版本:4.0

关于feed流:需要了解常见feed流架构。了解sort set。

关于sort set

见:https://redis.readthedocs.io/en/2.4/sorted_set.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 添加单个元素

redis> ZADD page_rank 10 google.com
(integer) 1

# 添加多个元素

redis> ZADD page_rank 9 baidu.com 8 bing.com
(integer) 2

redis> ZRANGE page_rank 0 -1 WITHSCORES
1) "bing.com"
2) "8"
3) "baidu.com"
4) "9"
5) "google.com"
6) "10"

# 添加已存在元素,且 score 值不变

redis> ZADD page_rank 10 google.com
(integer) 0

redis> ZRANGE page_rank 0 -1 WITHSCORES # 没有改变
1) "bing.com"
2) "8"
3) "baidu.com"
4) "9"
5) "google.com"
6) "10"

# 添加已存在元素,但是改变 score 值

redis> ZADD page_rank 6 bing.com
(integer) 0

redis> ZRANGE page_rank 0 -1 WITHSCORES # bing.com 元素的score值被改变
1) "bing.com"
2) "6"
3) "baidu.com"
4) "9"
5) "google.com"
6) "10"

Zrange 返回有序集key中,所有score值介于minmax之间(包括等于minmax)的成员。有序集成员按score值递增(从小到大)次序排列。

具有相同score值的成员按字典序(lexicographical order)来排列(该属性是有序集提供的,不需要额外的计算)。

可选的LIMIT参数指定返回结果的数量及区间(就像SQL中的SELECT LIMIT offset, count),注意当offset很大时,定位offset的操作可能需要遍历整个有序集,此过程最坏复杂度为O(N)时间。

score 聚合

在这里,我们先简单定义各个因子的权重:点赞数(like_count) > 回复数(reply_count) > 按时间排序(time)。

为了尽量减少我们score值占用的bit位。

为了描述清楚。这里用一个例子:当前评论,点赞数为 5 , 回复数为 8 ,时间戳为 12312312

默认 score为0。

  • 因子权重最低的放在小数位。其实时间戳并不连续,需要发号器(snowflake算法)使时间戳因子连续。现在的 score = 0.12312312 。
  • 第二维度为回复数。这里需要第一个计算手段:计算出当前数据库里面,回复数最多的那个评论的数目。假设当前最大回复数为n,这里取10000。在这里做最大数据保护,扩展100 倍,即,我们任务,未来会有一个评论的回复数会到100w(注意:只是保护设计,生产环境需要详细评估,基于你的用户体量来分析,考虑互联网的2、8分布)。此时的score = 8.12312312。
  • 第三个维度为点赞数。上一步的最大回复数用在这里。即第三个维度的score = 100w * 5 + 8 + 0.12312312

即 : score = like_count * ( max(reply_count) * 100) + reply_count + 0.time 。

推导过程

当用户有点赞时,即like_count > 0:

用户的 score = like_count * ( max(reply_count) * 100) + reply_count + 0.time > 0 * ( max(reply_count) * 100) + reply_count + 0.time >= reply_count + 0.time 。

即:当用户的评论有赞时,永远会按赞排序。

回复同理,当like_count = 0、 reply_count > 0 时:

score = like_count * ( max(reply_count) * 100) + reply_count + 0.time > 0 * ( max(reply_count) * 100) + 0* reply_count + 0.time >= 0.time 。

即:当用户的评论没有赞时,但有评论时、永远会按时间排序。

验证:当我们有三个评论

  • A (点赞 3, 评论 5,时间 100 ) 。 score = 3000005.100
  • B (点赞 4, 评论 5,时间 100 )。 score = 4000005.100
  • C (点赞 3, 评论 4,时间 99 )。 score = 3000004.99
1
2
3
4
5
6
7
8
9
10
11
12
13
redis> ZRANGE add 3000005.100 A 

redis> ZRANGE add 4000005.100 B

redis> ZRANGE add 3000004.99 C




redis> ZREVRANGE page_rank 0 -1 # bing.com 元素的score值被改变
1) "B"
2) "A"
3) "C"