使用MySQL、Redis解决排行榜问题

问题描述

排行榜主要分为两种,一种是并列排行榜(存在相同排名的情况),一种是严格排行榜(分先后顺序,不存在并列名次)。

一般根据不同的业务场景,选用不同的排行榜。例如,对于存在实物奖励且前一名与后一名的奖品差距很大时,往往采用严格排行榜,而对于只是激励用户的场景,则选用并列排行榜。

下面,通过举两个例子,介绍一下这两种排行榜。
假设存在一张表 tbl_score,原始数据如图:

tbl_score 表

用到的表和数据:

CREATE TABLE `tbl_score` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `score` int(11) DEFAULT NULL COMMENT '得分',
  `created_time` int(11) DEFAULT NULL COMMENT '得分时间',
  PRIMARY KEY (`id`)
) COMMENT='得分表';

INSERT INTO `tbl_score` (`id`, `score`, `created_time`)
VALUES
    (1, 90, 1534429870),
    (2, 80, 1534429871),
    (3, 82, 1534412273),
    (4, 96, 1534429243),
    (5, 100, 1534429133),
    (6, 5, 1534429273),
    (7, 8, 1534429823),
    (8, 80, 1534429801);

方式一:并列排行榜

经过排行后的排行结果如图:


并列排行榜

可以看到,相同得分情况下,名次相同。下一位,名次为当前用户顺序位。例如,两名第五名,则下一位为第七名,

方式二:严格排行榜

严格排行榜,通过多级排行,排列出有先后次数的排名。在本例中,当相同得分的情况下,根据时间进行二次排序,最先得分的名次越高。
排行结果如图:


严格排行榜,得分+时间

并列排行榜的解决方法

MySQL 实现

SELECT `id`, `score`,
    IFNULL((SELECT COUNT(*) FROM `tbl_score` WHERE `score` > `t`.`score`), 0) + 1 AS `rank`  # 计算并列排名
FROM `tbl_score` t
ORDER BY rank ASC

通过 (SELECT COUNT(*) FROM `tbl_score` WHERE `score` > `t`.`score`),获取排序。计算原理是比较出比当前分数要大的条数,然后加1,获取当前排序。例如,排名第一的得分,筛选出比当前大的得分行数为0,然后加1,得排名为 1。

逻辑层实现排行

// data 为已根据 score DESC 排列的数据

const ranked = data.map(function(item, i) {
    if (i > 0) {
        // 获取上一个元素
        var prevItem = data[i - 1];
        if (prevItem.score == item.score) {
            // 得分相同,则排序相同
            item.rank = prevItem.rank;
        } else {
            // 得分不相同,则排序 + 1
            item.rank = i + 1;
        }
    } else {
        // 第一个数据,排序为 1
        item.rank = 1;
    }

    return item;
});

精确排行榜的解决方法

MySQL 实现

根据多级条件进行排序,本例中以得分、时间为条件进行排序。优先对得分按照降序进行排序,其次是以时间升序进行排序。得分高且获得分数越早的玩家,排序越靠前。

最后,计算行号得到排名。

SELECT `id`,`score`, `created_time`, (@rowNum := @rowNum + 1) as rank
FROM tbl_score, (SELECT @rowNum := 0) b
ORDER BY `score` DESC, `created_time` ASC

Redis 实现

主要利用Redis的有序数列集合(zset)实现。

实现原理

有序集合由三部分组成,KEY(键)、score(成员的得分)、member(成员)。有序集合的每一项,都是以键值对的形式存储,每一项都有一个分数。有序集合会根据score自动排序。利用这个特性,我们就可以实现排行榜了。

scoro 是数字类型,可以是整形也可以是浮点型。按照排行榜多级排序的要求,相同分值下按照先来后到的顺序排序(创建时间越早,排序越高),但是Redis相同分值,是按照 memberASCII码进行排序。
如果直接将得分作为有序集合的 score,得不到我们想要的效果。

zset score 按照ASCII码排序

所以,需要将 score进行改造,同时记录得分与时间信息。实现方式有两种:

  1. score 为整数。得分 + 时间差。
  2. score 为浮点数。整数部分使用得分,小数部分使用时间差。

假设得分为 10, 得分时间为 1534649521(Unix时间戳。2018/8/19 11:32:01), 截止时间为 1534694400(Unix时间戳。2018/8/20 0:0:0。 若无截止时间,取值为 9999999999)。
方式一:

10 + (1534694400 - 1534649521) = 44889

方式二:

10.44879

选第二种方式的好处是,当需求不仅需要排序,还需展示得分时,可以将 score 强制转化成整形,即可获取到得分。需要注意的是,得分最好不要太大, score得分尽量控制在16位以下(浮点数时,小数点前后位数和不要超过16位,最好15位)。超过16位后,score值存入redis,会发生精度丢失。

超过16位,精度发生丢失

获取排行榜

ZREVRANGE test 0 -1 withscores

实现步骤

  1. 排行榜
  2. 相同得分,按照先来后到的顺序
  3. redis有序集合
  4. 计算score值
  5. 得到排序排行榜

总结

本文主要对并列排行榜和精确排行榜的问题进行了分析,采用Redis 或MySQL的方式解决排行榜问题。在使用Redis解决精确排行榜问题时,遇到了 score 精度问题。开发者需要特别注意此点,对于特别大的 score时,需要特别处理。例如,将大数值转换成小数值。

关于并列排行榜处理问题,在实践中仍然存在一些问题。例如,对于排序计算较复杂的场景。我们会使用定时任务提前计算好排行榜,然后将数据写入缓存中。这样,用户在拉取排行榜时能得到较快响应。

但是当既需要展示排名,又需要展示得分信息,且需要保证获取的数组是按序时,为了方便查询时,较快获取排名、得分。我采用的方式是,将排名、得分分别写入两个 zset中。理想状态下,期望写一个 zset,并同时储存排名、得分信息,且方便取出,又不会丢失精度。目前,没有找到这种解决办法。

参考文章

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容