線上排行榜通常由兩部分構(gòu)成:
1决瞳、排名最前的N位(假設(shè)前20名);
2左权、你所在的當前排位皮胡;
假設(shè)使用數(shù)據(jù)庫做用戶排名,比如MySql赏迟。假設(shè)已經(jīng)構(gòu)造了一張簡單的表格屡贺。
第一個問題比較好解決,執(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秒量没。
使用數(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名的賽手瞒津,又要獲取個人成績以及個人得分排名,代碼如下:
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毫秒間。