榜单系统

目前的各种常见应用都有榜单的身影,例如今日头条的新闻热榜、豆瓣的书籍电影榜单、qq音乐的音乐榜单、微信运动榜单等等。

系统开发领域(系统设计榜单系统)(1)

头条热榜

这些榜单是如何被设计的呢,如果单纯要做个简单的榜单系统并不困难,但是如果要考虑到这是一个国民级别的应用,在复杂的业务诉求下复杂度还是很高的。

所以,在考虑这个问题前,我们得明确业务的诉求:

榜单分类

榜单可以大致分为实时榜单和离线榜单两种:

积分维度

榜单的积分维度可能是多维度,比如说一个实时积分排行榜,可能是用户给作者的作品点赞、评论产生积分,不同的行为产生的权重不一样,且需要能够查看不同维度的积分总和。

离线榜单通常是存储了积分明细的底表或者各维度积分的汇总表,所以比较简单;实时榜单在设计上如果需要多维度就得考虑存储两份,一份是实时排序积分,一份是明细数据。下面分别看下两种榜单的实现方式。

离线榜单

离线榜单的实现比较简单,通常是有两个要素:

  1. 源数据:Hive、HDFS、MySQL等等存储
  2. 定时任务:一个定时任务程序,依据业务需求实现排序计算
  3. 目标存储:高性能的存储,提供给C端快速获取,比如 Redis,或者 MySQL 本地缓存

【关于定时任务计算的技术选型】

定时任务得依据原始的数据量以及计算时间复杂度、空间复杂度评估一下,如果是量级小的,直接用脚本在cron里跑就可以;但是如果量级比较大且计算耗时得上分布式计算了,Spark等大数据计算框架是不错的选择。

如何保证数据输出时榜单不受影响

因为数据写出的是一个 replace 的过程,需要将原有数据删除然后再写入,那么这个过程可能导致数据丢失。所以通常的解决办法是为每批数据写入指定一个版本号,然后有个字段标记当前ready的数据是哪个版本的。

只有当新的数据完全写入成功才更新读取版本号。这样就可以保证对用户无任何影响(因为写入可能也不是事务的)且可以保留几个版本号的数据,可以方便用于未来回溯问题,或者统计同环比。

系统开发领域(系统设计榜单系统)(2)

版本号控制更新

离线榜单优缺点

  1. 实现简单
  2. 数据一致性高
  3. 可支持非常复杂的积分计算逻辑
  1. 更新时延大,不利于部分业务(比如活动投票)使用
  2. 计算量级大,每次都需要全量重排,对于写入不频繁的应用会有较多资源浪费
实时榜单

实时榜单的实现相对会比较复杂一点,这里也要结合看下具体的业务场景。

分场景设计

小榜单,读写请求量级大

可以考虑使用 redis zset 来实现动态的榜单,有积分加减直接上操作。一致性不错,但是积分 incr 操作的时候可能会失败,所以不能保证绝对的强一致性,但是业务基本够用,读写性能强。

不过需要注意的是 redis zset 是单机存储的,如果请求量级超过了redis单机的约束会有问题,这个时候得考虑再加一层本地缓存了,或者加redis从节点,通过从节点来抗住读请求。

另外这种模式下,只能实现单维度的榜单,如果需要实现多维度的话可以在写入的时候用一个Hash存储指定项目的多维度值,使用HIncrby命令实现不同元素的计数,同时使用ZSet进行累加,但是这种模式难以保证 hash 和 Zset 中数据的一致性。

读写请求量级小

这种模式比较简单,可以直接用 mysql 存储,建立好索引,查询榜单的时候直接 order by 查询出来,走 B 树索引,性能还是会不错的;且写入可以由事务来保证强一致性;然后 mysql 本身都是磁盘存储,几百万甚至几千万的榜单大小无压力。

但是需要注意的是,mysql 读写请求量不宜过大。读可以通过备库来缓解压力,写入由于事务可能有间隙锁的原因,会互相影响。

大榜单,请求量级大

这种情况很多人会想到用多个zset然后做分片,但是因为我们这里需要做的是全局的排序,多个zset分片的话难以找到合适的分片方法。如果只是按照数据范围分片,那么数据可能需要做迁移,比如说原本是在 [0,10000] 这个 zset 上,积分突破了 10000 后 需要删除元素然后加到下个 zset 中,这需要引入锁、事务的实现,而且实现相对比较复杂。

还有一种模式,就是在业务上考虑做一点妥协,我们榜单虽然可能很长,但是业务上需要的具体名次的一般只有前10000个。

可以用 hash 定长 zset 的模式来实现,加积分逻辑图:

系统开发领域(系统设计榜单系统)(3)

同时,启动一个定时的程序,定时截断 zset, 理论上内存的榜单长度可能超过 Limit,业务系统获取时指定下 Limit 这样获取到的最长长度不可能超过 Limit 的配置。

这种模式的优点是性能很好,业务无锁、无事务实现。且性能稳定,读写性能都很高,写qps相当于单点redis瓶颈,读qps可通过从节点扩容。但是缺陷是业务上必须容忍没有全量实时排名,全量离线的也可以用hive离线产出。其次是 hash 和 redis 的数据一致性不能完全保证,只能是最终一致性,当然这个可以个离线的对账机制。

截断实现

redis zset 本身没提供截断的功能,我们可以自行在业务层做。写个定时任务:

  1. 判断当前元素个数超过 Limit 个
  2. 执行 `ZREMRANGEBYRANK` 把超过的 Rank 元素删除

具体可参考 Stack Overflow:stackoverflow/questions/49770648/how-to-limit-count-of-items-in-redis-sorted-sets

总结

榜单系统在业务开发中是一个相对核心的功能,不同的业务场景需要使用不同的技术栈。我们在设计时需要记得充分考虑数据量级、榜单大小、业务形态,以此才能确定出最好的那个技术方案。希望对你的工作有帮助!

,