【Redis实践】使用zset实现实时排行榜以及一些优化思考 ...

打印 上一主题 下一主题

主题 826|帖子 826|积分 2493

1.概述

我们在做互联网项目的时候会遇到一些排行版的需求,假如排行榜的时效性不高,好比日榜,周榜这种,可以思量通过定时使命统计、聚合数据并落库,必要查询的时候直接查询这个统计好的数据就好了。但有时候我们遇到的需求时效性会高一点,好比小时榜、分钟榜、乃至实时排行榜,这种情况下再使用定时使命统计的方式就不太符合了。
在Redis中有个叫zset的数据结构,非常适适用来做排名,它的数据结构中有一个score分数,我们可以直接使用Redis的指令,让里面的数据的按分数的大小进行排序。所以zset往往是我们做高时效性排行榜的解决方案。
2.zset的基本概念阐明

2.1.数据结构阐明

下面列举zset的操纵指令,有肯定经验的同砚看到这些指令就应该知道大概可以怎样使用了。
指令详细指令阐明zaddzadd key score member添加成员和分数,也可以更换成员分数zincrbyzincrby key score member为某个成员累加分数,假如成员不存在则创建成员zremzrem key member删除某个成员zscorezscore key member返回某个成员的分数zrangezrange key 0 -1 withscores按分值从小到大排zrevrangezrevrange key 0 -1 withscores按分值从大到小排 这里必要阐明一下的两个range方法,0 -1 是零和负一,中心用空格隔开,意思是获取所有的分数,假如是想获取指定命量的分数,例如top10,这里可以使用 0 9,最后一个withscores的意思的Redis会返回每个成员的分数。在没有这个选项的情况下,ZRANGE只会返回成员的名称,而不包括其对应的分数。
下面可以看看zset的使用方法。
2.2.zset做排行榜的指令

以一个例子来阐明,假设现在有3个用户和对应的分数分别如下:
  1. user1: 100
  2. user2: 200
  3. user3: 150
复制代码
现在就通过Redis的指令,来试一下排行榜功能,依次键入以下指令:
  1. zadd leaderboard 100 user1
  2. zadd leaderboard 200 user2
  3. zadd leaderboard 150 user3
  4. zrange leaderboard 0 -1 WITHSCORES
  5. zrevrange leaderboard 0 -1 WITHSCORES
复制代码

可以看到的是,返回的效果的是一行member,一行分数的结构,按照分数的高低进行排序的。
3. 项目中的实践

下面通过在通过RedisTemplate来封装一下排行榜的demo,然后会列出一些思考,思量实际存在的题目及其解决方案。
3.1.RedisTemplate实现排行榜

由于Redis在SpringBoot中的设置不是本章的重点,以下忽略设置。提供了几个简单的方法,分别是:


  • 添加或更换用户分数
  • 添加或更新用户分数
  • 获取排行榜前N名
  • 获取某个用户的排名
  • 删除指定用户
  1. import org.springframework.beans.factory.annotation.Autowired;
  2. import org.springframework.data.redis.core.RedisTemplate;
  3. import org.springframework.data.redis.core.ZSetOperations;
  4. import org.springframework.stereotype.Service;
  5. import java.util.Set;
  6. @Service
  7. public class LeaderboardService {
  8.     @Autowired
  9.     private RedisTemplate<String, String> redisTemplate;
  10.     private static final String LEADERBOARD_KEY = "leaderboard";
  11.     /**
  12.      * 添加或替换用户分数
  13.      */
  14.     public void addOrReplaceScore(String userId, double score) {
  15.         ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();
  16.         zSetOps.add(LEADERBOARD_KEY, userId, score);
  17.     }
  18.     /**
  19.      * 添加或更新用户分数
  20.      */
  21.     public void addOrUpdateScore(String userId, double score) {
  22.         ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();
  23.         zSetOps.incrementScore(LEADERBOARD_KEY, userId, score);
  24.     }
  25.     /**
  26.      * 获取排行榜前N名
  27.      */
  28.     public Set<ZSetOperations.TypedTuple<String>> getTopRanks(int topN) {
  29.         ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();
  30.         return zSetOps.reverseRangeWithScores(LEADERBOARD_KEY, 0, topN - 1);
  31.     }
  32.     /**
  33.      * 获取用户排名
  34.      */
  35.     public Long getUserRank(String userId) {
  36.         ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();
  37.         Long rank = zSetOps.reverseRank(LEADERBOARD_KEY, userId);
  38.         // 排名从1开始
  39.         return rank != null ? rank + 1 : null;
  40.     }
  41.     /**
  42.      * 删除指定用户
  43.      */
  44.     public void removeUser(String userId) {
  45.         redisTemplate.opsForZSet().remove(LEADERBOARD_KEY, userId);
  46.     }
  47. }
复制代码
方法封装好了之后,通过controller提供一个用户访问入口就可以了。下面讲一讲大概遇到的题目以及处理惩罚方案。
3.2.大概存在的题目及解决方案

3.2.1. 限定成员的数量

一个运动假如参与的人数多,就大概出来成员不绝不断膨胀的情况,但实际上我们对排行榜的需求往往只是必要前xx名的数据,例如前10名、前100名、前10000名等等。根据实际的需求,我们可以限定zset中的数量。假如现在保留一万名,就可以提供一个方法,整理排名一万以后的数据:
  1. // 限制排行榜最大长度
  2. private static final int MAX_RANKING_SIZE = 10000;
  3. /**
  4. * 清理低活跃数据
  5. */
  6. public void cleanUpInactiveUsers() {
  7.     ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();
  8.     Long memberCount = Optional.ofNullable(zSetOps.zCard(LEADERBOARD_KEY)).orElse(0L);
  9.     if (memberCount > MAX_RANKING_SIZE) {
  10.         zSetOps.removeRange(LEADERBOARD_KEY, 0, -MAX_RANKING_SIZE - 1);
  11.     }
  12. }
复制代码
这个方法可以在插入新的成员时调用,但是由于会多次操纵Redis,其实是不建议在保存排行榜分数的时候执行的,可以思量通过定时使命来处理惩罚,例如:
  1. @Component
  2. public class ScheduledTasks {
  3.     @Autowired
  4.     private LeaderboardService leaderboardService;
  5.     // 每天凌晨2点清理
  6.     @Scheduled(cron = "0 0 2 * * ?")
  7.     public void cleanInactiveUsersTask() {
  8.         leaderboardService.cleanUpInactiveUsers();
  9.     }
  10. }
复制代码
这里的天天破晓两点,可以根据必要调整为每小时整理一次,每10分钟整理一次等等。
3.2.2.保留当前分数与最高分数

zset中针对同一个用户只能保存一个分数,假如要实现保存当前分数和最高分数,可思量用两个zset来处理惩罚,处理惩罚方式也比力简单,按照:获取当前分数比力分数更新历史最高分数的顺序做就好了,下面是一个简单的代码:
  1. public void updateScore(String userId, double newScore) {
  2.     // 1. 获取当前分数
  3.     Double currentScore = redisTemplate.opsForZSet().score("currentLeaderboard", userId);
  4.     // 2. 更新当前分数
  5.     redisTemplate.opsForZSet().add("currentLeaderboard", userId, newScore);
  6.     // 3. 更新历史最高分数
  7.     if (currentScore == null || newScore > currentScore) {
  8.         redisTemplate.opsForZSet().add("highestLeaderboard", userId, newScore);
  9.     }
  10. }
复制代码
同样的,历史最高分数的zset也必要思量限定成员数量的题目。别的,假如要思量原子性,可以通过将上述的代码封装到lua脚本中执行。
3.2.3.批量操纵成员分数,减少并发

在并发较高的情况下,假如想减少Redis插入请求,我们可以在内存中先保存一部分的请求,等到达某个阈值的时候,再做Redis的插入操纵。这里阈值可以是积累了多少个成员做批量更新,也可以是积累到了肯定的时间,例如积累了一分钟的数据。
RedisTemplate中的add()有一个重载方法,可以传入一个set进行批量操纵:


这是一个interface,我们可以先实现一下:
  1. public class MemberValue<T> implements ZSetOperations.TypedTuple<T> {
  2.     private T value;
  3.     private Double score;
  4.     @Override
  5.     public T getValue() {
  6.         return value;
  7.     }
  8.     public void setValue(T value) {
  9.         this.value = value;
  10.     }
  11.     @Override
  12.     public Double getScore() {
  13.         return score;
  14.     }
  15.     public void setScore(Double score) {
  16.         this.score = score;
  17.     }
  18.     @Override
  19.     public int compareTo(ZSetOperations.TypedTuple<T> o) {
  20.         return 0;
  21.     }
  22. }
复制代码
然后以每50个成员更新一次为例,代码如下:
  1. private Set<ZSetOperations.TypedTuple<String>> memberSet = new HashSet<>();
  2. @Async
  3. public void asyncBatchSetScore(String userId, double score) {
  4.     MemberValue<String> memberValue = new MemberValue<>();
  5.     memberValue.setScore(score);
  6.     memberValue.setValue(userId);
  7.     synchronized (LeaderboardService.class) {
  8.         memberSet.add(memberValue);
  9.         if (memberSet.size() >= 50) {
  10.             ZSetOperations<String, String> zSetOps = redisTemplate.opsForZSet();
  11.             zSetOps.add(LEADERBOARD_KEY, memberSet);
  12.             memberSet.clear();
  13.         }
  14.     }
  15. }
复制代码
假如要修改阈值为时间,可以维护一个时间窗口,并修改判定条件即可,这里不展开了。
4.总结

本章先解说了zset的数据结构以及使用方式,然后通过RedisTemplate做了一个Demo,演示怎样实现排行榜,并对一些大概遇见的题目做了思考了解决方案。在开发中,可以选择其中的一些方案来解决实际的题目。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

大连全瓷种植牙齿制作中心

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表