当前位置: 首页 > news >正文

Redis实现排行榜

1、场景

对于在百科文库社区等项目中,统计统计用户的活跃度进行排名,比如

用户每访问一个新的页面 ;对于一篇文章,点赞、收藏 ;取消点赞、取消收藏,将之前的活跃分收回;文章评论 ;发布一篇审核通过的文章等。

2、Redis 如何实现排行榜?

Redis 实现排行榜主要依赖于其有序集合zset(Sorted Set)数据结构。

zset中可以存储不重复的元素集合,并为每个元素关联一个浮点数分数(score),Redis 会根据这个分数自动对集合中的元素进行排序。

使用有序集合-添加元素

可以使用 ZADD 命令来向有序集合中添加元素,将上面列表中:用户id作为元素、积分作为分数

上面命令中activity_rank是有序集合的名称,100、300、200 是每个用户的积分,user1、user2、user3 是用户的id

获取用户积分排行

使用 ZREVRANGE 命令(从高到低排序)或 ZRANGE 命令(从低到高排序)来获取排行榜的前几名

获取某个用户的积分

3、积分相同时,如何处理?

当用户积分相同时,要求按最后更新时间升序

可以将zset中的score设置为一个浮点数,其中整数部分为积分,小数部分为最后更新时间时间戳,算法如下

score = 积分 + 时间戳/10的13次方

这里为什么要除以10的13次方?由于时间戳的长度是13位,除以10的13次方,可以将其移到小数点的右边

4、SpringBoot代码实现

使用spring的事件监听机制+Redis Zset 实现次活跃度排行,后续如有需求日度、月度可将key加上时间,设置好过期时间即可,也可使用aop切面对接口地址切入来实现活跃度的增减,做成可配置的形式

像这样:

    private String todayRankKey() {return ACTIVITY_SCORE_KEY + DateUtil.format(DateTimeFormatter.ofPattern("yyyyMMdd"), System.currentTimeMillis());}/*** 本月排行榜** @return 月度排行榜key*/private String monthRankKey() {return ACTIVITY_SCORE_KEY + DateUtil.format(DateTimeFormatter.ofPattern("yyyyMM"), System.currentTimeMillis());}

引入maven配置


<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

源码如下:

  • UserActivityListener、ActivityScoreEvent:监听到用户行为,根据不同行为加减对应分值
  • UserActivityRankService:获取用户活跃度排行榜,添加活跃度
  • ActivityScoreController : 简单测试控制器接口

@RestController
@RequestMapping("/activityScore")
@RequiredArgsConstructor
public class ActivityScoreController {private final UserActivityRankService userActivityRankService;private final ApplicationEventPublisher applicationEventPublisher;@RequestMapping("/addActivityScore")public Boolean addActivityScore(@RequestBody ActivityScoreBo activityScore){activityScore.setUpdateTime(System.currentTimeMillis());ActivityScoreEvent activityScoreEvent = new ActivityScoreEvent(this,NotifyTypeEnum.COMMENT,  activityScore.getUserId(), activityScore.getUpdateTime());applicationEventPublisher.publishEvent(activityScoreEvent);return true;}@GetMapping("/userRankings")public List<UserRankingVo> userRankings(@RequestParam("topN") int topN){return   userActivityRankService.userRankings(topN);}
}

@Setter
@ToString
@EqualsAndHashCode(callSuper = true)
public class ActivityScoreEvent extends ApplicationEvent {/*** 通知类型*/private NotifyTypeEnum notifyType;/*** 用户ID*/private Long userId;/*** 最后更新时间(时间戳毫秒)*/private Long updateTime;public ActivityScoreEvent(Object source, NotifyTypeEnum notifyType, Long userId, Long updateTime) {super(source);this.notifyType = notifyType;this.userId = userId;this.updateTime = updateTime;}
}

/*** 用户活跃相关的消息监听器**/
@Component
@RequiredArgsConstructor
public class UserActivityListener {private final UserActivityRankService userActivityRankService;/*** 用户操作行为,增加对应的积分* 点赞 收藏 +1* 评论 回复 +2* 取消点赞 取消收藏  -1* 删除评论 删除回复 -2* @param msgEvent 用户行为*/@EventListener(classes = ActivityScoreEvent.class)@Asyncpublic void notifyMsgListener(ActivityScoreEvent msgEvent) {ActivityScoreBo activityScoreBo = new ActivityScoreBo().setUserId(msgEvent.getUserId()).setUpdateTime(msgEvent.getUpdateTime());switch (msgEvent.getNotifyType()) {case PRAISE:case COLLECT:userActivityRankService.addActivityScore(activityScoreBo.setScore(1) );break;case COMMENT:case REPLY:userActivityRankService.addActivityScore(activityScoreBo.setScore(2) );break;case CANCEL_COLLECT:case CANCEL_PRAISE:userActivityRankService.addActivityScore(activityScoreBo.setScore(-1) );break;case DELETE_COMMENT:case DELETE_REPLY:userActivityRankService.addActivityScore(activityScoreBo.setScore(-2) );break;default:}}}

/*** 用户活跃排行榜**/
public interface UserActivityRankService {/*** 添加活跃分* @param activityScore*/void addActivityScore(ActivityScoreBo activityScore);/*** 获取用户积分排行榜(倒序)** @param topN 前多少名*/List<UserRankingVo> userRankings( int topN);
}

@Slf4j
@Service
@RequiredArgsConstructor
public class UserActivityRankServiceImpl implements UserActivityRankService {private static final String ACTIVITY_SCORE_KEY = "activity_rank";private final StringRedisTemplate stringRedisTemplate;/*** 添加活跃分** @param activityScore 触发活跃积分的时间类型*/@Overridepublic void addActivityScore( ActivityScoreBo activityScore) {if (activityScore.getUserId() == null) {return;}Double currentScore = this.stringRedisTemplate.opsForZSet().score(ACTIVITY_SCORE_KEY, String.valueOf(activityScore.getUserId()));if (currentScore == null){currentScore = 0.0;}//先按积分降序,积分相同时按照最后更新时间升序,score = 积分 + (1 - 时间戳/10的13次方)double score = currentScore.intValue() + activityScore.getScore() + (1 - activityScore.getUpdateTime() / 1e13);this.stringRedisTemplate.opsForZSet().add(ACTIVITY_SCORE_KEY,String.valueOf(activityScore.getUserId()), score);}@Overridepublic List<UserRankingVo> userRankings(int topN) {Set<ZSetOperations.TypedTuple<String>> typedTuples = this.stringRedisTemplate.opsForZSet().reverseRangeWithScores(ACTIVITY_SCORE_KEY, 0, topN - 1);if (CollectionUtils.isEmpty(typedTuples)){return Collections.emptyList();}List<UserRankingVo> userRankingList = new ArrayList<>();for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {UserRankingVo userRankingVo = new UserRankingVo();userRankingVo.setUserId(typedTuple.getValue());userRankingVo.setScore(typedTuple.getScore() == null ? 0 : typedTuple.getScore());userRankingList.add(userRankingVo);}return userRankingList;}
}

对应一些实体:

@Data
public class UserRankingVo {private String userId;private double score;
}

@Data
@Accessors(chain = true)
public class ActivityScoreBo {/*** 目标用户ID*/private Long userId;/*** 增加的分数*/private Integer score;/*** 最后更新时间(时间戳毫秒)*/private Long updateTime;
}

@Getter
@AllArgsConstructor
public enum NotifyTypeEnum {COMMENT(1, "评论"),REPLY(2, "回复"),PRAISE(3, "点赞"),COLLECT(4, "收藏"),DELETE_COMMENT(1, "删除评论"),DELETE_REPLY(2, "删除回复"),CANCEL_PRAISE(3, "取消点赞"),CANCEL_COLLECT(4, "取消收藏"),;private final Integer code;private final String name;}

测试结果

http://www.lryc.cn/news/617403.html

相关文章:

  • 2025年渗透测试面试题总结-14(题目+回答)
  • 【MySQL基础篇】:MySQL索引——提升数据库查询性能的关键
  • 简单的身份验证中间件Tinyauth
  • 如何使用 Watchtower 实现定时更新 docker 中的镜像并自动更新容器(附 schedule 的参数详细解释)
  • 京东商品评论API秘籍!轻松获取商品评论数据
  • Go 语言三大核心数据结构深度解析:数组、切片(Slice)与映射(Map)
  • 【JSON】通俗易懂的JSON介绍
  • LangChain 框架 Parser 讲解
  • Spring Framework源码解析——InitializingBean
  • 基于数据结构用java实现二叉树的排序器
  • 零基础AI编程开发微信小程序赚流量主广告实战
  • Spring Framework源码解析——DisposableBean
  • 【PyTorch】单目标检测项目部署
  • 逃离城市与喧嚣,拥抱新的生活方式
  • 第2节 PyTorch加载数据
  • 5G与云计算对代理IP行业的深远影响
  • AI基础与实践专题:PyTorch实现线性回归
  • 开博尔雷电5数据线:120Gbps“闪电传输”,以Intel硬核基因从容优化数字生活
  • STM32CubeMX + HAL 库:用硬件IIC接口实现AT24C02 EEPROM芯片的读写操作
  • 【算法训练营Day23】贪心算法part1
  • InfluxDB 在物联网设备数据采集与分析中的应用(二)
  • Apache Ignite超时管理核心组件解析
  • 元数据管理与数据治理平台:Apache Atlas 基本搜索 Basic Search
  • 强化学习常用数据集
  • linux 秒 安装谷歌浏览器 区分ubuntu和centos 给python爬取网站使用
  • 提升行车安全的关键技术:BSD(盲点监测)与DSM(驾驶员监测)是如何工作的?
  • 剧本杀小程序系统开发:推动行业数字化转型新动力
  • 【VS Code - Qt】如何基于Docker Linux配置Windows10下的VS Code,开发调试ARM 版的Qt应用程序?
  • AI模型服务接入WAF防火墙
  • 为什么Open WebUI可以不联网问问题,而直接使用Ollama可能需要联网