Redis 实现用户积分和积分排行榜微服务优化

news/2024/11/20 23:16:26/

文章目录

      • 修改添加积分方法
      • 积分排行控制层redis实现
      • 积分排行业务逻辑层
      • Redis排行榜测试
      • 使用 JMeter 压测对比

在之前的博客中我通过 MySQL数据库实现了积分和积分排行榜功能,在数据量大和并发量高的情况下会有以下缺点:

  • SQL编写复杂;
  • 数据量大,执行统计SQL慢;
  • 高并发下会拖累其他业务表的操作,导致系统变慢;

使用 Sorted Sets 保存用户的积分总数,因为 Sorted Sets 有 score 属性,能够方便保存与读取,使用指令:

# 添加元素的分数,如果member不存在就会自动创建
ZINCRBY key increment member 
# 按分数从大到小进行读取
zrevrange key
# 根据分数从大到小获取member排名
zrevrank key member

修改添加积分方法

当将用户积分记录插入数据库后,同时利用ZINCRBY指令,将数据存入Redis中,这里不使用ZADD的原因是当用户不存在记录要插入,而且存在时需要将分数累加。
在这里插入图片描述

积分排行控制层redis实现

    /*** 查询前 20 积分排行榜,同时显示用户排名 -- Redis** @param access_token* @return*/@GetMapping("redis")public ResultInfo findDinerPointsRankFromRedis(String access_token) {List<UserPointsRankVO> ranks = userPointsService.findUserPointRankFromRedis(access_token);return ResultInfoUtil.buildSuccess(request.getServletPath(), ranks);}

积分排行业务逻辑层

  • 排行榜:从Redis中根据user:points的key按照score的排序进行读取,这里使用Redis的ZREVRANGE指令,但在ZREVRANGE指令只返回member,不返回score,在RedisTemplate的ZSetOperations中有一个一个API方法叫reverseRangeWithScores(key, start, end)其中start从0开始,返回的是member和score,底层是将ZREVRANGEZSCORE指令进行组装。
  • 个人排名:使用REVRANKZSCORE操作进行读取;
    /*** 查询前 20 积分排行榜,并显示个人排名 -- Redis** @param accessToken* @return*/public List<UserPointsRankVO> findUserPointRankFromRedis(String accessToken) {// 获取登录用户信息SignInUserInfo signInUserInfo = loadSignInUserInfo(accessToken);// 统计积分排行榜Set<ZSetOperations.TypedTuple<Integer>> rangeWithScores = redisTemplate.opsForZSet().reverseRangeWithScores(RedisKeyConstant.user_points.getKey(), 0, 19);if (rangeWithScores == null || rangeWithScores.isEmpty()) {return Lists.newArrayList();}// 初始化用户 ID 集合List<Integer> rankuserIds = Lists.newArrayList();// 根据 key:用户 ID value:积分信息 构建一个 MapMap<Integer, UserPointsRankVO> ranksMap = new LinkedHashMap<>();// 初始化排名int rank = 1;// 循环处理排行榜,添加排名信息for (ZSetOperations.TypedTuple<Integer> rangeWithScore : rangeWithScores) {// 用户IDInteger userId = rangeWithScore.getValue();// 积分int points = rangeWithScore.getScore().intValue();// 将用户 ID 添加至用户 ID 集合rankuserIds.add(userId);UserPointsRankVO userPointsRankVO = new UserPointsRankVO();userPointsRankVO.setId(userId);userPointsRankVO.setRanks(rank);userPointsRankVO.setTotal(points);// 将 VO 对象添加至 Map 中ranksMap.put(userId, userPointsRankVO);// 排名 +1rank++;}// 获取 users 用户信息ResultInfo resultInfo = restTemplate.getForObject(usersServerName +"findByIds?access_token=${accessToken}&ids={ids}",ResultInfo.class, accessToken, StrUtil.join(",", rankuserIds));if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {throw new ParameterException(resultInfo.getCode(), resultInfo.getMessage());}List<LinkedHashMap> dinerInfoMaps = (List<LinkedHashMap>) resultInfo.getData();// 完善用户昵称和头像for (LinkedHashMap dinerInfoMap : dinerInfoMaps) {ShortUserInfo shortDinerInfo = BeanUtil.fillBeanWithMap(dinerInfoMap,new ShortUserInfo(), false);UserPointsRankVO rankVO = ranksMap.get(shortDinerInfo.getId());rankVO.setNickname(shortDinerInfo.getNickname());rankVO.setAvatarUrl(shortDinerInfo.getAvatarUrl());}// 判断个人是否在 ranks 中,如果在,添加标记直接返回if (ranksMap.containsKey(signInUserInfo.getId())) {UserPointsRankVO rankVO = ranksMap.get(signInUserInfo.getId());rankVO.setIsMe(1);return Lists.newArrayList(ranksMap.values());}// 如果不在 ranks 中,获取个人排名追加在最后// 获取排名Long myRank = redisTemplate.opsForZSet().reverseRank(RedisKeyConstant.user_points.getKey(), signInUserInfo.getId());if (myRank != null) {UserPointsRankVO me = new UserPointsRankVO();BeanUtils.copyProperties(signInUserInfo, me);me.setRanks(myRank.intValue() + 1);// 排名从 0 开始me.setIsMe(1);// 获取积分Double points = redisTemplate.opsForZSet().score(RedisKeyConstant.user_points.getKey(),signInUserInfo.getId());me.setTotal(points.intValue());ranksMap.put(signInUserInfo.getId(), me);}return Lists.newArrayList(ranksMap.values());}

Redis排行榜测试

查询结果如下:
在这里插入图片描述

{"code": 1,"message": "Successful.","path": "/redis","data": [{"id": 1171,"nickname": "共饮一杯无","avatarUrl": null,"total": 773,"ranks": 1,"isMe": null},{"id": 482,"nickname": "共饮一杯无","avatarUrl": null,"total": 772,"ranks": 2,"isMe": null},{"id": 161,"nickname": "共饮一杯无","avatarUrl": null,"total": 762,"ranks": 3,"isMe": null},{"id": 740,"nickname": "共饮一杯无","avatarUrl": null,"total": 757,"ranks": 4,"isMe": null},{"id": 1629,"nickname": "共饮一杯无","avatarUrl": null,"total": 754,"ranks": 5,"isMe": null},{"id": 912,"nickname": "共饮一杯无","avatarUrl": null,"total": 747,"ranks": 6,"isMe": null},{"id": 213,"nickname": "共饮一杯无","avatarUrl": null,"total": 744,"ranks": 7,"isMe": null},{"id": 1477,"nickname": "共饮一杯无","avatarUrl": null,"total": 742,"ranks": 8,"isMe": null},{"id": 771,"nickname": "共饮一杯无","avatarUrl": null,"total": 737,"ranks": 9,"isMe": null},{"id": 791,"nickname": "共饮一杯无","avatarUrl": null,"total": 736,"ranks": 10,"isMe": null},{"id": 1989,"nickname": "共饮一杯无","avatarUrl": null,"total": 735,"ranks": 11,"isMe": null},{"id": 1027,"nickname": "共饮一杯无","avatarUrl": null,"total": 735,"ranks": 12,"isMe": null},{"id": 492,"nickname": "共饮一杯无","avatarUrl": null,"total": 734,"ranks": 13,"isMe": null},{"id": 1743,"nickname": "共饮一杯无","avatarUrl": null,"total": 733,"ranks": 14,"isMe": null},{"id": 1529,"nickname": "共饮一杯无","avatarUrl": null,"total": 729,"ranks": 15,"isMe": null},{"id": 242,"nickname": "共饮一杯无","avatarUrl": null,"total": 727,"ranks": 16,"isMe": null},{"id": 1126,"nickname": "共饮一杯无","avatarUrl": null,"total": 725,"ranks": 17,"isMe": null},{"id": 796,"nickname": "共饮一杯无","avatarUrl": null,"total": 719,"ranks": 18,"isMe": null},{"id": 418,"nickname": "共饮一杯无","avatarUrl": null,"total": 718,"ranks": 19,"isMe": null},{"id": 1435,"nickname": "共饮一杯无","avatarUrl": null,"total": 717,"ranks": 20,"isMe": null},{"id": 6,"nickname": "共饮一杯无","avatarUrl": null,"total": 627,"ranks": 172,"isMe": 1}]
}

可以看到id为6的用户排名172,同时展示排名前20名的数据。

使用 JMeter 压测对比

通过JMeter分别对数据库和Redis两种方式实现的积分排行榜进行压力测试(5000并发),可以发现Redis在响应速度,吞吐量上面都提升明显,同时异常率更低。

使用Sorted Sets优势

  • Redis本身内存数据库,读取性能高;
  • Sorted Sets底层是SkipList + ZipList既能保证有序又能对数据进行压缩存储;
  • Sorted Sets操作简单,几个命令搞定;

Redis Sorted Sets是类似Redis Sets数据结构,不允许重复项的String集合。不同的是Sorted Sets中的每个成员都分配了一个分数值(score),它用于在Sorted Sets中进行成员排序,从最小值到最大值。Sorted Sets中所有的成员都是唯一的,其分数(score)是可以重复的,即是说一个分数可能会对应多个值。
用Sorted Sets可以非常快的进行添加、删除、或更新成员,其复杂度是O(m*log(n)),m是添加或查询的成员数量。因为成员是按照顺序添加的,所以可以非常快的通过score或者索引进行范围查询。访问Sorted Sets中间的元素也是非常快的,因此可以用sort sets作为一个不重复的小型有序列表。 通过Sorted Sets可以快速操作任何你想做的事情:排序成员,判断成员是否在集合中,快速访问集合中间的成员。
总的来说,在其他数据库比较难完成的任务,用Sorted Sets可以更快更优性能的完成。
更多Sorted Sets的用法可以查看官方文档。

本文内容到此结束了,
如有收获欢迎点赞👍收藏💖关注✔️,您的鼓励是我最大的动力。
如有错误❌疑问💬欢迎各位指出。
主页:共饮一杯无的博客汇总👨‍💻

保持热爱,奔赴下一场山海。🏃🏃🏃


http://www.ppmy.cn/news/12490.html

相关文章

Pytorch优化器全总结(四)常用优化器性能对比 含代码

目录 写在前面 一、优化器介绍 1.SGDMomentum 2.Adagrad 3.Adadelta 4.RMSprop 5.Adam 6.Adamax 7.AdaW 8.L-BFGS 二、优化器对比 优化器系列文章列表 Pytorch优化器全总结&#xff08;一&#xff09;SGD、ASGD、Rprop、Adagrad Pytorch优化器全总结&#xff08;二…

Python【3】:格式化输出

文章目录前言1. 通过 %\%% 实现1.1. 语法1.2. 常见格式化符号1.3. 格式化操作符辅助指令2. 通过 format() 函数实现2.1. 语法2.2. 用法2.2.1. 基本用法2.2.2. 数字格式化前言 在做题与实际应用中&#xff0c;经常会出现需要格式化&#xff08;输出&#xff09;字符串的情况&am…

走近软件生态系统

生态系统&#xff08;Ecosystem&#xff09;原本是一个生物学术语&#xff0c;意思是由一些生命体相互依存、相互制约而形成的大系统&#xff0c;就像我们学生时代在生物学课堂上学到的那样。隐喻无处不在&#xff0c;人们把这个术语移植到了 IT 领域中来&#xff0c;比如我们常…

【阶段四】Python深度学习09篇:深度学习项目实战:循环神经网络处理时序数据项目实战:CNN和RNN组合模型

本篇的思维导图: 项目背景 时间序列数据集中的所有数据都伴随着一个时戳,比如股票、天气数据。这个数据集,是科学家们多年间用开普勒天文望远镜观察并记录下来的银河系中的一些恒星的亮度。广袤的宇宙,浩瀚的星空在过去很长一段时间里,人类是没有办法证明系外行星…

【Linux系统】第四篇:Linux中编辑器vim的使用

文章目录一、vim的介绍1、vim的基本模式2、vim的使用二、命令模式1、光标移动操作2、文本复制、粘贴、剪切、撤销操作3、文本编辑相关操作三、插入模式四、底行模式底行模式命令集五、vim的配置原理六、sudo无法提权问题一、vim的介绍 vim是Linux下的一款多模式编辑器。 注意…

U-Boot 之零 源码文件、启动阶段(TPL、SPL)、FALCON、设备树

最近&#xff0c;工作重心要从裸机开发转移到嵌入式 Linux 系统开发&#xff0c;在之前的博文 Linux 之八 完整嵌入式 Linux 环境、&#xff08;交叉&#xff09;编译工具链、CPU 体系架构、嵌入式系统构建工具 中详细介绍了嵌入式 Linux 环境&#xff0c;接下来就是重点学习一…

C++ 简单实现RPC网络通讯

RPC是远程调用系统简称&#xff0c;它允许程序调用运行在另一台计算机上的过程&#xff0c;就像调用本地的过程一样。RPC 实现了网络编程的“过程调用”模型&#xff0c;让程序员可以像调用本地函数一样调用远程函数。最近在做的也是远程调用过程&#xff0c;所以通过重新梳理R…

迭代器模式

迭代器模式 1.迭代器模式基本介绍 迭代器模式&#xff08;Iterator Pattern&#xff09;是常用的设计模式&#xff0c;属于行为型模式 如果我们的集合元素是用不同的方式实现的&#xff0c;有数组&#xff0c;还有 java 的集合类&#xff0c;或者还有其他方式&#xff0c;当客…