使用NodeJS+Redis 實現(xiàn)千萬級用戶量的排行榜

線上排行榜通常由兩部分構(gòu)成:
1决瞳、排名最前的N位(假設(shè)前20名);
2左权、你所在的當前排位皮胡;
假設(shè)使用數(shù)據(jù)庫做用戶排名,比如MySql赏迟。假設(shè)已經(jīng)構(gòu)造了一張簡單的表格屡贺。


用戶分數(shù)表

第一個問題比較好解決,執(zhí)行以下語句即可:

select * from ob_appstaff order by score limit 20

但第二點有點困難了锌杀,例如:

explain SELECT u.* FROM
(
SELECT t.*, @rownum := @rownum + 1 AS rownum
FROM (SELECT @rownum := 0) r,
(SELECT * FROM ob_appstaff ORDER BY score DESC) AS t
) AS u WHERE u.id = 1;      //u.id 是用戶的ID

查看一下這個分析結(jié)果就會發(fā)現(xiàn)僅僅為了計算出某個用戶的排行榜排名甩栈,執(zhí)行了一次聚簇索引和一次IO操作的非聚簇索引,甚至在第三次時做了全表掃描糕再。最后我在一個只有20萬數(shù)據(jù)的表中執(zhí)行了操作后大約花費時間2秒量没。

分析結(jié)果

使用數(shù)據(jù)庫做大數(shù)據(jù)的排行榜排名,自然不是一種好的方案突想。 如果只有單機的情況下殴蹄,并且有興趣做技術(shù)挑戰(zhàn)究抓,可以走全局變量的列表堆實現(xiàn)自動排序+二分法查找的排名。這樣的排行效率最優(yōu)饶套,當然也得接受健壯性考驗漩蟆。

Redis 實現(xiàn)排行榜方案

怎么合理地把用戶分數(shù)加進redis里? 要特別注意同分的排行情況妓蛮。假設(shè)用戶A和用戶B 都是90.5分怠李。通常碰到同分情況下,我們可能會以誰優(yōu)先得分蛤克,誰往前排的想法來思考捺癞。所以很自然,在入庫時應該要把入庫時間也寫到里面去构挤。示例代碼如下:

        let pipeline = app.redis.get('local').multi();
        let name = `用戶名`;
        let rank = 90.5  // 用戶分數(shù)
        const max_time = 9999999999; //相當于時間:2286-11-21 01:46:39 

        //用最大時間髓介,減去當前時間,就會獲取較大數(shù)的排名筋现。
        let rank_in_db = rank+(max_time - parseInt(Date.now()/1000)) / 1000000000000;

       pipeline.zadd("RANK-TEST",rank_in_db,name);
       await pipeline.exec();

寫一個示例唐础,可以壓1000萬個用戶數(shù)據(jù)進庫吧(根據(jù)實際機器性能決定 ,機器性能不足容易引發(fā)堆棧溢出錯誤):

        //基于EggJS框架以及egg-redis組件

        const { ctx,app} = this;
        const start_time = (new Date()).valueOf();

        const max_length = 10000000;
        const max_time = 9999999999; //相當于時間:2286-11-21 01:46:39 
               let total = await app.redis.get('local').zcard("RANK-TEST");
        console.log(`當前已經(jīng)有數(shù)據(jù):${total}條.`);
        if(total>= max_length){
            console.log('夠1000萬了矾飞,不需要創(chuàng)建了.');
            return;
        }
         let pipeline = app.redis.get('local').multi();
        //每次只創(chuàng)造100萬個數(shù)據(jù)
        for(let i = 0; i<max_length;i++){
            let name = `fan-${i+1}`;     //創(chuàng)造虛擬用戶
            let rank = parseFloat((Math.random()*60+40).toFixed(2));
           //隨造制作2位數(shù)的分數(shù)一膨,

            //通過用最大時間,減去當前時間洒沦,就會獲取較大數(shù)的排名豹绪。
            let rank_in_db = rank+(max_time - parseInt(Date.now()/1000)) / 1000000000000;

            pipeline.zadd("RANK-TEST",rank_in_db,name);
            current_time=null;
            rank=null;
            name=null;
        }
        await pipeline.exec();
        const end_time = (new Date()).valueOf();

        console.log(`推入1000萬條數(shù)據(jù)的總耗時:${end_time-start_time} 毫秒。`);

以下圖為示例申眼,既找出前10名的賽手瞒津,又要獲取個人成績以及個人得分排名,代碼如下:


TOP 10 成績排名
        const { ctx,app} = this;

        const start_time = (new Date()).valueOf();
        const current_user = "fan-904404";

        const redis = app.redis.get('local');

        let total = await redis.zcard("RANK-TEST");

        //let total = "1000百萬";
        //zrevrange 按照最高成績排名
       // zrange  按照最低成績排名
        const list =await  redis.zrange("RANK-TEST",1,10,'WITHSCORES');

        console.log(`參與排行的總用戶數(shù):${total}`);
        console.log('其中前十名的排行榜:');
        let rank = 1;
        for(let index = 0;index < list.length; index +=2){
            console.log(`第${rank}名是:${list[index]}括尸,分數(shù):${list[index+1].toFixed(2)}`);
            rank++;
        }
        rank = await  redis.zrank("RANK-TEST",current_user);

        let score = await  redis.zscore("RANK-TEST",current_user);

        console.log(`當前用戶:${current_user} 的排名:第${rank}名巷蚪,分數(shù):${score.toFixed(2)}`);

        const end_time = (new Date()).valueOf();

        console.log(`一共耗時:${end_time-start_time} 毫秒。`);

最后在本機測試濒翻,redis 數(shù)據(jù)庫在局域網(wǎng)內(nèi)屁柏,1000萬數(shù)據(jù)的查詢執(zhí)行一次總時長在10~30毫秒間。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末肴焊,一起剝皮案震驚了整個濱河市前联,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌娶眷,老刑警劉巖似嗤,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異届宠,居然都是意外死亡烁落,警方通過查閱死者的電腦和手機乘粒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來伤塌,“玉大人灯萍,你說我怎么就攤上這事∶看希” “怎么了旦棉?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長药薯。 經(jīng)常有香客問我绑洛,道長,這世上最難降的妖魔是什么童本? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任真屯,我火速辦了婚禮,結(jié)果婚禮上穷娱,老公的妹妹穿的比我還像新娘绑蔫。我一直安慰自己,他們只是感情好泵额,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布配深。 她就那樣靜靜地躺著,像睡著了一般梯刚。 火紅的嫁衣襯著肌膚如雪凉馆。 梳的紋絲不亂的頭發(fā)上薪寓,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天亡资,我揣著相機與錄音,去河邊找鬼向叉。 笑死锥腻,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的母谎。 我是一名探鬼主播瘦黑,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼奇唤!你這毒婦竟也來了幸斥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤咬扇,失蹤者是張志新(化名)和其女友劉穎甲葬,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體懈贺,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡经窖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年坡垫,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片画侣。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡冰悠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出配乱,到底是詐尸還是另有隱情溉卓,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布搬泥,位于F島的核電站的诵,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏佑钾。R本人自食惡果不足惜西疤,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望休溶。 院中可真熱鬧代赁,春花似錦、人聲如沸兽掰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽孽尽。三九已至窖壕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間杉女,已是汗流浹背瞻讽。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留熏挎,地道東北人速勇。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像坎拐,于是被迫代替她去往敵國和親烦磁。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容