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

目录

为什么要使用redis?

日榜、周榜实现方案介绍

热度增加代码参考

日榜获取

周榜获取


为什么要使用redis?

如果使用mysql

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

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

  • 优点 就是代码操作简单,有事项就执行sql语句修改一下热度值。
  • 缺点 就是频繁的数据库操作会对服务器造成一定的压力,效率也比较低。

redis实现

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

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

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

ZSet 的有关底层数据结构是 压缩列表、packlist、跳表。可以在去看看其他的博客了解详情。


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

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

zincrby key increment member

来实现一个热度的 原子递增 来保证线程安全。

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

(听说王者荣耀的排行榜就是用 Redis 实现的)

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

虽然 Redis mysql 都可以进行持久化,但是 Redis 的持久化主要是为了 保护数据,防止宕机等原因造成数据丢失。

在运行时,Redis 还是需要将所有的数据加载到内存当中。而 mysql 大部分数据是在 硬盘 当中的,当需要进行加载的时候,进行 IO 操作,将对应的页内容加载进内存操作。

当然 我们知道 mysqlinnoDB 引擎是基于 B+ 树存储的。像 B+ 树 的 根节点数据内容一般也是常驻内存的。

这个问题我举一个关于 家里空间使用的例子 帮助大家简单理解下这个问题。

这里将 卧室 比作 内存客厅 比作 磁盘

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

另外,在内存的数据还可能因为服务器宕机等原因而丢失数据。

日榜、周榜实现方案介绍

既然是排行榜就要有区分排行榜 每一天 或 每一个周 的根据,我们通过操作 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 为 30 天,(TTL 视业务而定,如月榜,那么数据就需要存储30天)避免垃圾数据占用内存
Redis 提供了 ZSet 的合并操作,我们可以用来实现周、月排行榜。如周榜就是 合并前 7 天的key 或者 上一周 7 天的 key

代码参考

热度增加代码参考

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

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

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

日榜获取

日 key算法:

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

获取日榜信息

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

周榜获取

周 key算法

这里是获取上一周 7 天的算法,而不是 前7天

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

获取周榜信息

	@Override
	public 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 key
		long dayKey = RankKeyUtils.getDayKey();
		// 计算需要进行聚合的上周一的 day key
		long 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();
			// 拿到 blogId
			Integer 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

前端使用 Vue3 编写。

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

  • 31
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

durancer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值