线上排行榜通常由两部分构成:
1、排名最前的N位(假设前20名);
2、你所在的当前排位;
假设使用数据库做用户排名,比如MySql。假设已经构造了一张简单的表格。
第一个问题比较好解决,执行以下语句即可:
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
查看一下这个分析结果就会发现仅仅为了计算出某个用户的排行榜排名,执行了一次聚簇索引和一次IO操作的非聚簇索引,甚至在第三次时做了全表扫描。最后我在一个只有20万数据的表中执行了操作后大约花费时间2秒。
使用数据库做大数据的排行榜排名,自然不是一种好的方案。 如果只有单机的情况下,并且有兴趣做技术挑战,可以走全局变量的列表堆实现自动排序+二分法查找的排名。这样的排行效率最优,当然也得接受健壮性考验。
Redis 实现排行榜方案
怎么合理地把用户分数加进redis里? 要特别注意同分的排行情况。假设用户A和用户B 都是90.5分。通常碰到同分情况下,我们可能会以谁优先得分,谁往前排的想法来思考。所以很自然,在入库时应该要把入库时间也写到里面去。示例代码如下:
let pipeline = app.redis.get('local').multi();
let name = `用户名`;
let rank = 90.5 // 用户分数
const max_time = 9999999999; //相当于时间:2286-11-21 01:46:39
//用最大时间,减去当前时间,就会获取较大数的排名。
let rank_in_db = rank+(max_time - parseInt(Date.now()/1000)) / 1000000000000;
pipeline.zadd("RANK-TEST",rank_in_db,name);
await pipeline.exec();
写一个示例,可以压1000万个用户数据进库吧(根据实际机器性能决定 ,机器性能不足容易引发堆栈溢出错误):
//基于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(`当前已经有数据:${total}条.`);
if(total>= max_length){
console.log('够1000万了,不需要创建了.');
return;
}
let pipeline = app.redis.get('local').multi();
//每次只创造100万个数据
for(let i = 0; i<max_length;i++){
let name = `fan-${i+1}`; //创造虛拟用户
let rank = parseFloat((Math.random()*60+40).toFixed(2));
//随造制作2位数的分数,
//通过用最大时间,减去当前时间,就会获取较大数的排名。
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万条数据的总耗时:${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(`参与排行的总用户数:${total}`);
console.log('其中前十名的排行榜:');
let rank = 1;
for(let index = 0;index < list.length; index +=2){
console.log(`第${rank}名是:${list[index]},分数:${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}名,分数:${score.toFixed(2)}`);
const end_time = (new Date()).valueOf();
console.log(`一共耗时:${end_time-start_time} 毫秒。`);
最后在本机测试,redis 数据库在局域网内,1000万数据的查询执行一次总时长在10~30毫秒间。