問題描述
排行榜主要分為兩種黑竞,一種是并列排行榜(存在相同排名的情況)捕发,一種是嚴格排行榜(分先后順序,不存在并列名次)很魂。
一般根據(jù)不同的業(yè)務(wù)場景扎酷,選用不同的排行榜。例如遏匆,對于存在實物獎勵且前一名與后一名的獎品差距很大時法挨,往往采用嚴格排行榜,而對于只是激勵用戶的場景幅聘,則選用并列排行榜凡纳。
下面,通過舉兩個例子帝蒿,介紹一下這兩種排行榜荐糜。
假設(shè)存在一張表 tbl_score
,原始數(shù)據(jù)如圖:
用到的表和數(shù)據(jù):
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);
方式一:并列排行榜
經(jīng)過排行后的排行結(jié)果如圖:
可以看到葛超,相同得分情況下暴氏,名次相同。下一位巩掺,名次為當前用戶順序位偏序。例如,兩名第五名胖替,則下一位為第七名研儒,
方式二:嚴格排行榜
嚴格排行榜,通過多級排行独令,排列出有先后次數(shù)的排名端朵。在本例中,當相同得分的情況下燃箭,根據(jù)時間進行二次排序冲呢,最先得分的名次越高。
排行結(jié)果如圖:
并列排行榜的解決方法
MySQL 實現(xiàn)
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`)
敬拓,獲取排序。計算原理是比較出比當前分數(shù)要大的條數(shù)裙戏,然后加1乘凸,獲取當前排序。例如累榜,排名第一的得分营勤,篩選出比當前大的得分行數(shù)為0,然后加1,得排名為 1葛作。
邏輯層實現(xiàn)排行
// data 為已根據(jù) score DESC 排列的數(shù)據(jù)
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 {
// 第一個數(shù)據(jù)赂蠢,排序為 1
item.rank = 1;
}
return item;
});
精確排行榜的解決方法
MySQL 實現(xiàn)
根據(jù)多級條件進行排序绪穆,本例中以得分、時間為條件進行排序客年。優(yōu)先對得分按照降序進行排序霞幅,其次是以時間升序進行排序。得分高且獲得分數(shù)越早的玩家量瓜,排序越靠前司恳。
最后,計算行號得到排名绍傲。
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 實現(xiàn)
主要利用Redis的有序數(shù)列集合(zset
)實現(xiàn)扔傅。
實現(xiàn)原理
有序集合由三部分組成,KEY(鍵)烫饼、score(成員的得分)猎塞、member(成員)。有序集合的每一項杠纵,都是以鍵值對的形式存儲荠耽,每一項都有一個分數(shù)。有序集合會根據(jù)score自動排序比藻。利用這個特性铝量,我們就可以實現(xiàn)排行榜了。
scoro 是數(shù)字類型银亲,可以是整形也可以是浮點型慢叨。按照排行榜多級排序的要求,相同分值下按照先來后到的順序排序(創(chuàng)建時間越早务蝠,排序越高)拍谐,但是Redis相同分值,是按照 member
的 ASCII
碼進行排序馏段。
如果直接將得分作為有序集合的 score
轩拨,得不到我們想要的效果。
所以院喜,需要將 score
進行改造亡蓉,同時記錄得分與時間信息。實現(xiàn)方式有兩種:
- score 為整數(shù)够坐。得分 + 時間差寸宵。
- score 為浮點數(shù)。整數(shù)部分使用得分元咙,小數(shù)部分使用時間差梯影。
假設(shè)得分為 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 強制轉(zhuǎn)化成整形,即可獲取到得分膳灶。需要注意的是咱士,得分最好不要太大, score
得分盡量控制在16位以下(浮點數(shù)時轧钓,小數(shù)點前后位數(shù)和不要超過16位序厉,最好15位)。超過16位后毕箍,score
值存入redis弛房,會發(fā)生精度丟失。
獲取排行榜
ZREVRANGE test 0 -1 withscores
實現(xiàn)步驟
- 排行榜
- 相同得分文捶,按照先來后到的順序
- redis有序集合
- 計算score值
- 得到排序排行榜
總結(jié)
本文主要對并列排行榜和精確排行榜的問題進行了分析,采用Redis 或MySQL的方式解決排行榜問題牺堰。在使用Redis解決精確排行榜問題時拄轻,遇到了 score
精度問題。開發(fā)者需要特別注意此點伟葫,對于特別大的 score
時恨搓,需要特別處理。例如筏养,將大數(shù)值轉(zhuǎn)換成小數(shù)值斧抱。
關(guān)于并列排行榜處理問題,在實踐中仍然存在一些問題渐溶。例如辉浦,對于排序計算較復(fù)雜的場景。我們會使用定時任務(wù)提前計算好排行榜茎辐,然后將數(shù)據(jù)寫入緩存中宪郊。這樣掂恕,用戶在拉取排行榜時能得到較快響應(yīng)。
但是當既需要展示排名弛槐,又需要展示得分信息懊亡,且需要保證獲取的數(shù)組是按序時,為了方便查詢時乎串,較快獲取排名店枣、得分。我采用的方式是叹誉,將排名鸯两、得分分別寫入兩個 zset
中。理想狀態(tài)下长豁,期望寫一個 zset
钧唐,并同時儲存排名、得分信息匠襟,且方便取出逾柿,又不會丟失精度。目前宅此,沒有找到這種解決辦法机错。