admin 管理员组

文章数量: 1086019

基于Redis中 Zset数据类型实现各类高性能排行榜日榜、周榜(附上整合SpringBoot示例代码)

目录

为什么要使用redis?

日榜、周榜实现方案介绍

热度增加代码参考

日榜获取

周榜获取


为什么要使用redis?

如果使用mysql

要实现排行榜功能,如果使用 mysql或其它关系型数据库 来进行排行,我们大致的思路就是在这个表再定义出一个字段 如hot 来记录热度值,然后在数据库查询时通过 order by hot 来实现排行。如果有影响排行的操作就直接 update 修改数据库表。

这样做的优缺点也很明显,就拿博客排行而言,可能很多操作都会影响到热度的变化。如浏览、评论、点赞等等。

优点就是代码操作简单,有事项就执行sql语句修改一下热度值。

缺点就是频繁的数据库操作会对服务器造成一定的压力,效率也比较低。

redis实现

Redis中一共有 5 种数据类型,其中 ZSet 是一个比较特殊的数据类型,

我们可以先了解 zset 的大体结构,

zset 数据的value有多列,每列的成员包含 memberscore

对于实现排行榜来说,一般member存放对应排行内容单项的 id 值,有相关的操作修改 score

score(分值)可以 来对存入数据进行一个排序。并通过

zincrby key increment member

来实现一个热度的原子递增。

那么使用 redis 来实现这个功能的优势已经不言而喻了,感觉 zset 这个数据类型就是为排行而生的,同时 redis 作为一项高性能 nosql 也更高地提高了程序的并发性能,效率也高很多。

redis为什么快,却又不能替代mysql?

这个问题我举一个例子让大家简单理解下这个问题。

类似于我们躺在房间里要拿东西(数据),如果东西就在房间里(服务器内存里),我们只要一伸手就能拿到。如果东西放在客厅或者储物间里(硬盘中),我们则需要走到外面去(效率低),才能拿到这项东西。但是房间里的空间有限内存小),不能存放太多的物品。所以在东西多的时候,还是需要把大部分东西放在客厅或者储物间mysql)。同时如果房间里东西多了(内存较满),也会影响我们的通行影响执行效率)。

日榜、周榜实现方案介绍

既然是排行榜就要有区分排行榜每一天或每一个周的根据,我们通过操作 key 的后缀来实现

redis key生成策略

通过获取时间戳,除以每日的单位 1000 * 60 * 60 * 24 得到一个 day key ,积累今日用户操作进行的分值累积
通过算法算出上周的 week key,对上周 day key 进行累积合并操作生成
得到的 day key 或 week key 将用来作为排行榜存入redis的 key 后置参数

(假设daykey 为 5,那么就生成 key 为 rank_day:5),redis可视化工具中:将为我们自动分文件夹

创建key时,指定 TTL 为 40天,避免垃圾数据占用内存
通过合并操作,来实现周、月排行榜

热度增加代码参考

热度增加,在当日的 key上进行操作,如果接口请求日榜就返回当日信息,如果请求的是周榜就新型合并操作

这里进行是否创建判断主要是为了 设置 TTL,避免造成空间浪费

	@Overridepublic void addRankHotScore(Integer blogId, Double score) {// 获取dayKeylong dayKey = RankKeyUtils.getDayKey();//封装 redis keyString key = RANK_HOT_DAY_KEY + dayKey;//进行判断,是否已经创建 该redisif (Boolean.FALSE.equals(redisTemplate.hasKey(key))) {//如果没有创建,就执行创建并设置 TTL 为40天redisTemplate.opsForZSet().incrementScore(key, blogId, score);redisTemplate.expire(key, RANK_HOT_DAY_TTL, TimeUnit.SECONDS);}//已创建,将对应博客热度增加 3redisTemplate.opsForZSet().incrementScore(key, blogId, score);}

日榜获取

日 key算法:

	/*** 获取 day key*/public static long getDayKey() {return System.currentTimeMillis() / (1000 * 60 * 60 * 24);}

获取日榜信息

	@Overridepublic List<RankHotVO> getTodayHotRank() {// 1 进行判断 redis 是否已经创建了 今天的redis 排行榜缓存的key// 1.1 获取 day keylong dayKey = RankKeyUtils.getDayKey();// 获取redis数据Set<ZSetOperations.TypedTuple<Integer>> typedTuples = redisTemplate.opsForZSet().reverseRangeWithScores(RANK_HOT_DAY_KEY + dayKey, 0, -1);return getRankHotVOList(typedTuples);}

周榜获取

周 key算法

	/*** 获取上一周的 week key*/public static long getWeekKey() {// 拿到 week key,和day keylong week = System.currentTimeMillis() / (1000 * 60 * 60 * 24 * 7);long day = getDayKey();// 时间戳取模到周key,不是刚好从周一算起的,如果dayKey%7==4,这样这天才是刚好周一// 由于原来的week key并不是从周一开始算起,所以进行移位计算,拿到上周的week keylong key = day % 7;if (key >= 4) {// 经计算,取模值大于等于 4 时 进入新的一周,将week key-1return week - 1;}//否则,在新的一周的周四时,week值将再加1,所以要拿到上一周的week key需要-2return week - 2;}

获取周榜信息

	@Overridepublic List<RankHotVO> getWeekHotRank() {long weekKey = RankKeyUtils.getWeekKey();// 拿到数据集合Set<ZSetOperations.TypedTuple<Integer>> weekRank = redisTemplate.opsForZSet().reverseRangeWithScores(RANK_HOT_WEEK_KEY + weekKey, 0, -1);// 进行判断查看有没有数据if (!(weekRank == null || weekRank.size() == 0)) {// 如果拿到数据直接进行查询并返回return getRankHotVOList(weekRank);}// 如果没有拿到数据集,进行合并操作并存入缓存// 获取 day keylong dayKey = RankKeyUtils.getDayKey();// 计算需要进行聚合的上周一的 day keylong mondayKey = dayKey % 7;if (mondayKey >= 4) {mondayKey = dayKey - (3 + mondayKey);} else {mondayKey = dayKey - (10 + mondayKey);}// 聚合上周的day key,存入集合ArrayList<String> dayKeys = new ArrayList<>();for (long i = mondayKey; i < mondayKey + 7; i++) {dayKeys.add(RANK_HOT_DAY_KEY + i);}// 合并操作获取合并结果redisTemplate.opsForZSet().unionAndStore("COUNT_WEEK", dayKeys, RANK_HOT_WEEK_KEY + weekKey);// 设置过期时间为 一星期redisTemplate.expire(RANK_HOT_WEEK_KEY + weekKey, RANK_HOT_WEEK_TTL, TimeUnit.SECONDS);weekRank = redisTemplate.opsForZSet().reverseRangeWithScores(RANK_HOT_WEEK_KEY + weekKey, 0, -1);return getRankHotVOList(weekRank);}

这其实有和日榜差不多,主要区别就是需要统计几天的数据进行合并操作

上述 return中方法主要就是根据 id 进行数据项查找

	/*** 拿到博客id及热度后,设置 热度排行榜显示 的相关参数** @param typedTuples 排行数据* @return 热点排行数据列表*/private List<RankHotVO> getRankHotVOList(Set<ZSetOperations.TypedTuple<Integer>> typedTuples) {if (typedTuples == null || typedTuples.size() == 0) {// 如果没有创建,说明今天暂无热榜相关的信息,返回空信息return null;}// 2 如果创建了,封装响应信息//拿到set集合迭代器Iterator<ZSetOperations.TypedTuple<Integer>> iterator = typedTuples.iterator();List<RankHotVO> result = new ArrayList<>();// 创建博客id集合List<Integer> blogIdList = new ArrayList<>();while (iterator.hasNext()) {//拿到这项信息ZSetOperations.TypedTuple<Integer> tuple = iterator.next();// 拿到 blogIdInteger blogId = tuple.getValue();// 2.1 将博客id存入集合blogIdList.add(blogId);// 2.2 设置热度信息RankHotVO rankHotVO = new RankHotVO();rankHotVO.setHot(tuple.getScore());result.add(rankHotVO);}List<Blog> blogList = blogMapper.selectBatchIds(blogIdList);// 创建 用户id集合List<Integer> userIdList = new ArrayList<>();for (Blog blog : blogList) {// 拿出 userId 存入集合userIdList.add(blog.getAuthorId());}// 查询并设置 user信息Map<Integer, UserDTO> userList = userClient.getUserList(userIdList).getData();for (int i = 0; i < blogList.size(); i++) {// 设置用户相关信息result.get(i).setBlog(blogList.get(i));result.get(i).setAuthor(userList.get(blogList.get(i).getAuthorId()));}return result;}

为了提高效率,可以先对需要查找的 id 进行统计,然后一次查找,减少数据库压力。

最后可以看看我的开源项目: i集大校园(类似于一个定位为校园里的微博)

i集大校园软件服务端,基于SpringCloud Alibaba 微服务组件及部分分布式技术实现服务之间关联及协作进行前后端分离项目实现。计划实现微信小程序和app两端同步。

使用技术栈为:Spring Boot、Spring Cloud Alibaba、rabbitMQ、JWT、minIO、mysql、redis、ES、docker、Jenkins、mybatis-plus

前端使用 微信小程序编写。

欢迎一起参加开源贡献和star项目哈!

本文标签: 基于Redis中 Zset数据类型实现各类高性能排行榜日榜周榜(附上整合SpringBoot示例代码)