使用Redis构建高效稳定低延迟的排行榜业务
Posted YQS_Love
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用Redis构建高效稳定低延迟的排行榜业务相关的知识,希望对你有一定的参考价值。
一、业务描述
现有一排行榜业务,数据库中拥有百万级用户,中秋佳节将要来临,用户可以写一篇关于中秋的故事,故事可邀请好友点赞,也可以打赏该故事,现在要挑选出关于中秋话题相关的故事,根据用户故事的点赞数、获得的打赏数以及故事查看次数为依据,根据一定规则算出后在客户端展示前100的故事排行榜。
二、业务分析
分析业务得知,用户故事排行榜可不必实时刷新,比如5分钟或者是10分钟刷新一次,用户是没有多少感知的,如果涉及数据量太过庞大或者是业务计算复杂需要更多时间排行,完全可以明确的告知用户5分钟或者10分钟刷新一次排行榜,甚至是更久。面对这样的业务,不可能在用户请求的时候去计算排行,这样做服务器不仅响应慢,服务器还会消耗大量的资源,比如大量的逻辑计算导致服务器CPU爆满,内存消耗过大等问题。要想用户的请求能够很快的响应,在查询时这个排行榜就已经排好了。下面我介绍一种我在工作中所使用的一种方法,也许并不是最好的,但满足一般的业务还是没有任何问题的,对于超大的数据集,需要另觅方法。
三、方法介绍
合理使用Redis的有序集合。Redis是一种将数据放到内存中的数据库,面对排行榜这样的业务需求,找它是完全没有问题的。更值得拍手叫好的是,Redis的有序集合可以帮助我们实现排行榜,将多个维度的数据源取得之后,通过一定的规则计算出分值,把这些数据放入到Redis的有序集合中,那么排行结果自然而然的就出来了,取出排行数据后,在填充一些客户端展示需要的数据即可。要想实现每隔一分钟或是5分钟刷新一次,只需开启一个定时器,定时去调用排行计算逻辑。更多关于Redis的有序集合的命令,请参考链接 http://doc.redisfans.com/ 或是Redis官网 https://redis.io/ 。
四、具体实现
实例以上诉故事业务为题,列出部分代码,仅供参考。
(1) 获取故事并计算排行分值。从多个数据库中取得关于中秋相关话题的故事,得到故事后在根据故事的id取得故事的一些统计信息,比如故事点赞数、获得的打赏数、查看数等。(注意:在将分值放到Redis时,一定要将故事Id与分值一一对应,否则在取数据时并不知道分值对应的故事是谁)
/**
* 计算排行榜分值
*/
public void calStoryRankList()
// 取得故事
List<Story> storyList = storyService.getFestivalStoryList();
if (CollectionUtils.isEmpty(storyList))
return;
// 得到故事的id
List<Long> storyIds = levyArticleStoryList.stream()
.map(story -> story.getId())
.collect(Collectors.toList());
// 根据故事id批量取得故事的统计信息
Map<Long, StoryStat> storyStats = statService.getStoryStats(storyIds);
// 保存根据排行规则得到的分值
Map<Long, Double> storiesRankMap = new HashMap<>(levyArticleStoryList.size());
// 计算排行分值
storyList.stream().forEach(story ->
StoryStat storyStat = stats.get(story.getId());
if (null != storyStat)
Long priseCount = storyStat.getPriseCount();
Long beansCount = bbStoryStat.getTotalEarning();
Double score = priseCount + beansCount / 100.0;
// 保留一位小数
score = Math.round(score * 10) / 10.0;
storiesRankMap.put(story.getId(), score);
);
// 将分值放到Redis中
rankListDao4Redis.putStoryRankList(storiesRankMap);
(2) 将计算好的排行分值放到Redis中。这里需要考虑几个问题,如果排行榜在第一次刷新时A故事是存在的,但在下次刷新时A故事被作者或者是运营人员删除了,那么Redis中应该去除这条废弃的数据,因此,这里我们需要使用Redis的删除键命令将这个键下的所有数据删除,在存入新的数据,如果新的数据存入失败,那么之前的删除操作应该回滚,此时可以使用Redis的事务控制。如果不这样做,会导致排行榜出现空的情况。详情见代码。
/**
* 设置征文排行数据
*
* @param stories
* @return
*/
public Boolean putStoryRankList(Map<Long, Double> stories)
// 根据规则生成Redis的key键
String rankKey = RedisKeys.keyOfStoryRankList();
// 自行在Service层注入jedis
Pipeline pipelined = jedis.pipelined();
// 开启事务
pipelined.multi();
// 删除以前的数据
pipelined.del(rankKey);
// 存入新的值
pipelined.zadd(rankKey , stories);
pipelined.exec();
pipelined.sync();
return true;
(3) 取得排行榜。有了前面的铺垫,取得排行榜就变得非常简单了。此处没有给全分页的实现,有兴趣的自己尝试做一下。实现如下:
public Map<Long,Double> getStoryRankList(int size, int offset)
// 根据规则生成Redis的key键
String rankKey = RedisKeys.keyOfStoryRankList();
Pipeline pipelined = jedis.pipelined();
// 取得排行榜,可以加入分页,需要注意Redis的取数据时根据数组下表来取的,因此,如果按照mysql数据库的规则,那么size需要减去1
Response<Set<Tuple>> scores = pipelined.zrevrangeWithScores(levyRankKey, offset, size - 1);
// 保存排行结果的Map
Map<Long,Double> result = new HashMap<>();
for (Tuple score : scores.get())
result.put(Long.parseLong(score.getElement().toString()), score.getScore()));
return result;
(4) 填充数据。因为从Redis得到的排行榜数据仅仅包含故事的Id,很明显,这样的数据用户是无法识别的,因此,在此步仅仅是根据故事的ID去填充排行展示所需要的数据,具体业务,就得具体分析,此处不在给出代码。
五、总结
从这个例子看出,Redis给我们带了很大遍历,减少了实现业务的难度和工作量。倘若你所在的公司没有使用Redis(这样的情况应该不存在),那么要想实现一个高效且稳定的排行榜业务,还是得有两把刷子,比如自己使用缓存实现。Redis给我提供的便利不仅仅是这些,还有很多有用的东西等待我们去挖掘,本篇博文仅仅是给没有蓝图的同僚一个小小的启发,由于小编能力有限,文中难免有纰漏之处,还望指出,谢谢!
以上是关于使用Redis构建高效稳定低延迟的排行榜业务的主要内容,如果未能解决你的问题,请参考以下文章