如何設(shè)計一個排行榜?你學(xué)會了嗎?
前言
大家好,我是田螺。
最近有位星球粉絲問:田螺哥,如何設(shè)計一個排行榜?
日常開發(fā)中,我們經(jīng)常需要涉及設(shè)計排行榜的需求,如禮物排行榜、微信運(yùn)動排行、王者榮耀段位排行榜等等。今天我?guī)Т蠹伊牧模判邪袢绾卧O(shè)計。
圖片
數(shù)據(jù)庫的order by
很多小伙伴,一提到排行榜,就想到數(shù)據(jù)庫的order by。
其實這個思路是沒有錯的,我們假設(shè)用order by來實現(xiàn)一個簡單的微信運(yùn)動步數(shù)排行榜。
假設(shè)表結(jié)構(gòu)如下:
CREATE TABLE `user_info` (
`id` int NOT NULL AUTO_INCREMENT,
`user_name` varchar(255) DEFAULT NULL,
`age` int DEFAULT NULL,
`step` int DEFAULT 0 comment '步數(shù)',
`picture_url` varchar(255) DEFAULT NULL comment '頭像url',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3
這時候要實現(xiàn)一個微信步數(shù)排行版,不是有手就行嘛:
select *
from user_info
order by step desc
這個實現(xiàn)沒有問題的,如果表的數(shù)據(jù)量少的話,反而推薦這樣實現(xiàn)。如果數(shù)據(jù)量多呢?
我們先來造一千萬數(shù)據(jù)。
for (int i = 0; i < 10000000; i++) {
UserInfo record = new UserInfo();
record.setUserName("user" + i);
record.setAge(10);
record.setUserName("userName" + i);
record.setStep(new Random().nextInt(1000) * 1000);
record.setPictureUrl("https://tianluo/picture/"+i+".jpg");
userInfoDao.insertUserInfo(record);
}
我們現(xiàn)在查詢步數(shù)前1000的用戶,SQL如下:
select *
from user_info
order by step desc limit 0,1000;
然后看下耗時,set @@profiling=1;,show profiles;
圖片
可以發(fā)現(xiàn)耗時14秒多,這個耗時太久了,這種情況,我們可以添加索引優(yōu)化它。
ALTER TABLE user_info ADD INDEX idx_step (step);
添加完索引之后,發(fā)現(xiàn)再去查步數(shù)前1000的用戶,0.005秒:
圖片
這時候感覺,order by做分頁也挺好的。但是這個是你加了索引和限制排序條數(shù)的前提下啦。
實際上,當(dāng)數(shù)據(jù)量較小且查詢不頻繁時,可以使用 MySQL 的 order by來實現(xiàn)排行榜。而當(dāng)數(shù)據(jù)量較大且需要實時更新并頻繁查詢時,使用 Redis 的有序集合更為適合。
Redis 的 zset
有序集合zset是 Redis 提供的一種數(shù)據(jù)結(jié)構(gòu),它類似于集合(set),但每個成員都關(guān)聯(lián)著一個分?jǐn)?shù)(score),Redis 使用這個分?jǐn)?shù)來對集合中的成員進(jìn)行排序。
大家可以看看官網(wǎng)對它的簡介哈:
https://redis.io/commands/zadd/#sorted-sets-101
圖片
我們繼續(xù)搞一個微信運(yùn)動排行版。假設(shè)redis的key是 run:ranking,然后小明的步數(shù)是1000,小華的步數(shù)是3000,小紅的步數(shù)是2000,小李的步數(shù)是4000步,我們可以用zadd添加到redis,如下:
zadd run:ranking 1000 “小明”
zadd run:ranking 3000 “小華”
zadd run:ranking 2000 “小紅”
zadd run:ranking 4000 “小李”
我們用這個命令,就可以輸出排行榜啦:
ZREVRANGE run:ranking 0 -1 WITHSCORES
- ZREVRANGE 表示從大到小排序
- ZRANGE 表示從小到大排序
- ZADD key score1 member1 score2 member2…表示向集合中添加元素
它在Redis類似這樣保存的:
圖片
如果在redis中的score分?jǐn)?shù)相同,它是如何排序的呢?
在 Redis 的有序集合(Sorted Set)中,如果多個成員具有相同的分?jǐn)?shù),則它們按照字典順序進(jìn)行排序。具體來說,當(dāng)有多個成員具有相同的分?jǐn)?shù)時,Redis 會根據(jù)成員的字典順序(lexicographical order)對它們進(jìn)行排序。
字典順序是一種字符串比較的方式,它按照字母表的順序進(jìn)行排序。對于 ASCII 字符集,字母表的順序是 A-Z,a-z,數(shù)字的順序是 0-9。如果成員具有相同的分?jǐn)?shù),那么它們將按照它們的字典順序進(jìn)行排列。
例如,假設(shè)有一個名為 myset 的有序集合,包含了以下成員和分?jǐn)?shù):
"a" - 1000
"b" - 2000
"c" - 2000
"d" - 3000
在這個示例中,成員 "b" 和 "c" 具有相同的分?jǐn)?shù) 2000。當(dāng)使用 ZRANGE 或 ZREVRANGE 命令來查看有序集合的成員時,它們將按照字典順序進(jìn)行排列。因此,成員 "b" 將在成員 "c" 的前面,因為 "b" 的字典順序小于 "c"。
如果每個人看到的排行榜,都是一樣的,類似上面的處理就可以啦。如果每個人看到的排行榜不一樣的,我們則可以在redis zset的key,加上用戶標(biāo)志,比如userId即可。
使用 Redis 的有序集合作為排行榜,也有一些問題需要注意哈:
- 內(nèi)存消耗:有序集合中的數(shù)據(jù)會存儲在 Redis 的內(nèi)存中。如果排行榜的數(shù)據(jù)量非常大,可能會占用大量的內(nèi)存資源,導(dǎo)致 Redis 實例的內(nèi)存使用率增加。
- 數(shù)據(jù)更新頻繁:如果排行榜的數(shù)據(jù)需要頻繁更新,例如每秒鐘都有大量的用戶分?jǐn)?shù)變化,這可能會導(dǎo)致 Redis 的寫入負(fù)載增加,影響系統(tǒng)的性能。
- 網(wǎng)絡(luò)開銷:如果客戶端需要頻繁地向 Redis 發(fā)送更新排行榜數(shù)據(jù)的請求,可能會增加網(wǎng)絡(luò)開銷,尤其是在高并發(fā)的情況下。
- 數(shù)據(jù)一致性:在多個客戶端同時更新排行榜數(shù)據(jù)時,可能會出現(xiàn)數(shù)據(jù)不一致的情況。雖然 Redis 提供了一些原子操作來確保數(shù)據(jù)的一致性,但在高并發(fā)的情況下,仍然需要注意數(shù)據(jù)一致性的問題。
- 持久化:默認(rèn)情況下,Redis 將數(shù)據(jù)存儲在內(nèi)存中,并且可以通過持久化功能將數(shù)據(jù)寫入磁盤。但是,使用持久化功能可能會影響 Redis 的性能,并增加系統(tǒng)的負(fù)載。
- 高可用性:如果 Redis 實例發(fā)生故障或者網(wǎng)絡(luò)中斷,可能會影響排行榜的可用性。為了確保排行榜的高可用性,需要考慮使用 Redis 的主從復(fù)制、哨兵模式或者集群模式來實現(xiàn)數(shù)據(jù)的備份和故障切換。