面试遇到一个问题:
现在有百万的数据,要对用户答题做一个排行榜,展示前20的排名信息,用户可以重复进行答题,更新分数排名。

一. 导数据入缓存

要实时展示一个用户排行榜,如果每次都重数据库查询数据,效率肯定不行。这是考虑到使用Redis缓存。
Redis的缓存类型主要有String,Hash,List,Set,ZSet这5种。对于要有序不重复的排行场景,采用ZSet,其中以分数作为score。
从数据库导入缓存这里有个点要考虑: 数据量很大无法一次性完成操作?
可以根据用户id分区间,每次取一批次的数据导入缓存中。 大体伪代码如下:

// 用户区间步长
int step=10000;
// 起始用户id
int startUserId=1;
whille(true){
	// 查询用户分数数据
	List dataList=selectUserScore(startUserId);
	if(dataList.size==0){
		break;
	}
	// 插入缓存中
	redis.add(dataList);
	// 更新起始用户id
	startUserId+=step;
}

二. 查询排名前20的数据

查询前多少的排名,redisTemplate有现成的方法:range。默认情况下是排序是从小到大的,要输出分数最高的可用倒叙输出。
这里要注意一点: ZSet的排名默认是0开始的,故而返回的排名要+1

public Set<Object> listRank() {
        BoundZSetOperations zSetOps = redisTemplate.boundZSetOps(RANK_KEY);
        return zSetOps.reverseRange(0, 19);
    }

三. 更新用户分数

更新用户分数主要考虑两点:

  • 同用户的分数如何实时更新
  • 不同用户分数一致的情况下,如何定义排名
1. 同用户的分数如何实时更新?
  • 首先将新的用户分数数据入数据库
  • 获取缓存当前用户的排行信息
  • 如果缓存中不存在或者分数低于最新的分数则,更新缓存
2. 不同用户分数一致的情况下,如何定义排名?

不同用户分数相同的情况,应以先达到该分数的用户排名靠前。考虑到Zset中score是double类型的,可以在小数上做文章。
用户分数数据入数据库之后能获取到对应的id,先插入的id较小,可以Integer.MAX_VALUE-id的方式设置小数。最终存入redis中的score格式: 用户分数.(Integer.MAX_VALUE-该数据id)

public void updateScore(String userCode,int score) {
       // 模拟存入数据库
       Random random = new Random();
       int i = random.nextInt(1000);
       BoundZSetOperations zSetOps = redisTemplate.boundZSetOps(RANK_KEY);
       // 当前分数, 分数.Integer.MAX_VALUE-数据库id 用于相同分数下排名;分数相同的用户,先获取到该分数的用户排名更高
       BigDecimal currentScore = new BigDecimal(score+"."+(Integer.MAX_VALUE-i));
       // 获取历史的分数
       Double scoreHistory = zSetOps.score(userCode);
       // 不存在该用户记录或者 历史分数比当前分数小,则更新缓存数据
       if (scoreHistory == null || BigDecimal.valueOf(scoreHistory).compareTo(currentScore) <=0) {
           zSetOps.add(userCode, currentScore.doubleValue());
       }
   }

四. 优化点

如果只需要的前20排名,不需要后面的的排名数据,在redis中存储全部的排行还是有所浪费内存的。
在更新时,获取分数排行20的数据分数,如果当前数据小于该数不更新缓存,否则更新缓存。
之后在判断缓存中总数是否达到50条,如果达到则删除小于排行20分数的数据。
为啥取存50条数据?
50只是一个估计值,也可以40,100等,主要要大于20个数。如果只存20条数据,必然要加全局锁,来保证数据的准确。这里让数据冗余了一部分,既可以避免频繁的移除数据,也可以避免加锁的问题。

最后奉上 项目demo地址