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)
    @Async
    public 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 触发活跃积分的时间类型
     */
    @Override
    public 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);
        }

    @Override
    public 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;

}

测试结果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值