在之前呢,我们设计了一个积分功能,积分很重要的一个作用就是来参与排行,那今天我们就来设计一下排行榜的功能。
1.业务分析
一般在项目中,排行榜都会有两种
• 实时榜单:也就是本赛季的榜单(学霸天梯榜)
• 历史榜单:也就是历史赛季的榜单
1.1实时榜单
实时榜单通常指的是我们当前的榜单数据,要想形成榜单,我们在查询数据库时,需要先对用户分组,再对积分求和,最终按照积分和排序
要知道,每个用户都可能会有数十甚至上百条积分记录,当用户规模达到百万规模,可能产生的积分记录就是数以亿计。要在每次查询排行榜时,在内存中对这么多数据做分组、求和、排序,对内存和CPU的占用会非常恐怖,不太靠谱。此时我们又可以考虑使用我们的Redis来保存榜单数据了。
Redis中有一种类型是带有排序功能的也就是ZSet,所以我们选择这个类型进行存储。由于内存是十分珍贵的,我们在存储的时候选择可以只选择存储用户id,每当用户得到积分后,将对应用户的score+1,这样就可以进行一个排序的作用。
1.2历史榜单
历史榜单是曾经的某个月最后一天的排名结果,积分和排名已经确定,没有实时更新排序的需求。
我们可以在每个月第一天的凌晨,使用定时任务,将上个月最终的排行榜数据写入到数据库,然后就可以清空redis中上个月的积分和榜单数据了。
2.功能实现
2.1实时积分榜
2.1.1生成积分榜
此功能很简单,只需要将保存到数据库的内容同时保存给Redis即可,具体可参考下图:
2.1.2查询积分榜
一般在我们用户的个人中心,可以查看指定赛季的排行榜,还可以查看自己的排名,并且排行榜还分为实时榜单和历史榜单,我们需要在这一个接口中实现这两类的查询。
需要注意的是:
无论是历史榜单还是当前榜单,结构都一样,分为两部分:
-
榜单数据,就是N个用户的积分、排名形成的集合
-
当前用户的积分和排名,当前用户不一定上榜,因此需要单独查询
2.2历史积分榜
由于历史榜单不会再变动,所以我们可以选择将redis中的榜单再写回数据库,我们需要完成三个步骤:
-
创建历史赛季表(一个赛季一张表)
-
从redis中将数据保存到mysql中对应的赛季表中
-
删除redis中历史赛季的数据
上面三个定时任务应该使用xxl-job实现,而且要规定好执行顺序(任务链)
2.2.1生成积分榜
在天机学堂项目中,积分排行榜是分赛季的,每一个月是一个赛季。因此每到每个月的月初,就会进入一个新的赛季。所有用户的积分应该清零,重新累积。
但是,我们能把Redis中的榜单数据直接清空吗?显然不行!Redis中的榜单数据是上个月的数据,属于历史榜单了,直接清空就丢失了一个赛季的数据。
因此,我们必须将Redis中的历史数据持久化到数据库中,然后再清零。如图:
不过,这里就有一个问题需要解决:
假如有数百万用户,这就意味着每个赛季榜单都有数百万数据。随着时间推移,历史赛季越来越多,如果全部保存到一张表中,数据量会非常恐怖!
由于我们要解决的是数据过多问题,因此分表的方式选择水平分表。具体来说,就是按照赛季拆分,每一个赛季是一个独立的表,如图:
不过这里我们可以做一些简化:
-
我们可以将id采用自增id,那么id就是排名,排名字段就不需要了。
-
不同赛季用不同表,那么赛季字段就不需要了。
由于每个赛季都有一张表,显然我们应该在月初进行创建表,因此我们可以使用定时任务创建。
2.2.2清理缓存
添加清理Redis缓存任务,这里不使用del ,否则可能因为大数据量删除阻塞主线程,而是使用unlink命令开启异步线程删除。
2.2.3任务链
现在,所有任务都已经定义完毕。接下来就给配置任务调度了。我们最终期望的任务执行顺序是这样的:
XXL-JOB提供了子任务功能,可以直接完成此需求。
3.优化方案
3.1海量数据存储
对于海量数据存储的方案有很多常见的有:
3.1.1分区
表分区(Partition)是一种数据存储方案,可以解决单表数据较多的问题,MySQL5.1开始支持表分区功能。
分区就是按照某种规则,把表数据对应的ibd文件拆分成多个文件进行存储来存储。
从物理上来看,一张表的数据被拆到多个表文件存储了;从逻辑上来看,他们对外表现是一张表。
增删改查的方式不会有什么变化,只不过底层MySQL底层的处理上会有变更,例如检索时可以只检索某个文件,而不是全部。
优点:
• 可以存储更多的数据,突破单表上限,甚至可以存储到不同磁盘,突破磁盘上限
• 查询时可以根据规则只检索某一个文件,提高查询效率
• 数据统计时,可以多文件并行统计,最后汇总结果,提高统计效率
• 对于一些历史数据,如果不需要时,可以直接删除分区文件,提高删除效率
缺点:
-
分区字段必须是索引字段
-
分区方式不够灵活
-
只支持水平分区
3.1.2分表
分表是一种表设计方案,由开发者在创建表时按照自己的业务需求拆分表。一旦做了分表,无论是逻辑上,还是物理上,就从一张表变成了多张表,增删改查的方式就发生了变化,必须自己考虑要去哪张表做数据处理。
水平分表
例如,对于赛季榜单,我们可以按照赛季拆分为多张表,例如每一个赛季一张新的表,水平分表仅仅是每张表数据不同。但结构一样,查询赛季1,就找第一张表。查询赛季2,就找第二张表。
垂直分表
如果一张表的字段非常多,比如达到30个以上,这样的表我们称为宽表。
宽表由于字段太多,单行数据体积就会非常大,虽然数据不多,但可能表体积也会非常大!从而影响查询效率。
这个时候一张表就变成了两张表。而且两张表的结构不同,数据也不同。这种按照字段拆分表的方式,称为垂直拆分。
优点
• 拆分方式更加灵活
• 可以解决单表字段过多和数据过多的问题
缺点:
• 增删改查时,需要自己判断访问哪张表
• 垂直拆分还会导致事务问题及数据关联问题:原本一张表的操作,变为多张表操作。
3.3.3分库和集群
无论是分库还是分区,都是在单独的对一个数据库的操作,但是单个数据库也存在一些问题:
-
单点故障问题:数据库发生故障,整个系统就会瘫痪
-
单库的性能瓶颈问题:单库受服务器限制,其网络带宽、CPU、连接数都有瓶颈
-
单库的存储瓶颈问题:单库的磁盘空间有上限,如果磁盘过大,数据检索的速度又会变慢
综上,在大型系统中,我们除了要做分表、还需要对数据做分库,建立综合集群。
首先,在微服务项目中,我们会按照项目模块,每个微服务使用独立的数据库,因此每个库的表是不同的,这种分库模式称为垂直分库。
而为了保证单节点的高可用性,我们会给数据库建立主从集群,主节点向从节点同步数据。两者结构一样,可以看做是水平扩展。
这个时候就会出现垂直分库、水平扩展的综合集群,如图:
优点:
• 解决了海量数据存储问题,突破了单机存储瓶颈
• 提高了并发能力,突破了单机性能瓶颈
• 避免了单点故障
缺点:
• 成本非常高
• 数据聚合统计比较麻烦
• 主从同步的一致性问题
• 分布式事务问题