利用redis实现获取top N动态变化的数据

本文探讨了两种处理客户端上报数据并实时维护TOPN列表的方法。方案一是使用MySQL,但在高并发下可能导致磁盘IO过高和内存占用大。方案二是利用Redis的ZSet和Set数据结构,实现高效读写和动态排序,减少了数据库瓶颈和内存压力。通过Redis的差异操作,可以轻松找出新增和移除的客户端并通知。同时,对于对象附加属性,建议使用Hash结构存储。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

有一个这样的需求:
客户端不间断上报数据到后台服务器,后台服务判断TOP N里面是否包含当前上报的客户端,其中TOP N来源于上报数据值为前N的客户端。即这个top N数据是不断在变化的。 若客户端在TOP N中则可以继续上报,否则停止上报,如果之前属于TOP N,但后面又被移除的要告知客户端停止上报,新加入的要告知继续上报。
在这里插入图片描述
方案1:mysql

  1. 服务端收集客户端上报的数据同步写到mysql的一张表中。
  2. 存在就按客户端唯一标识更新数据,不存在就插入一条。
  3. 指定一个字段column A用来排序。
  4. 开启一个定时任务,定时刷新column A 为 top N的数据到一个列表List。
  5. 每次刷新列表List之前都要和旧的列表做对比,找出差异。

此方案简单,但是缺点有以下几点:

  1. 高并发情况下直接写mysql导致磁盘IO过高,数据库会是瓶颈。
  2. 每次都要判断是否存在。
  3. 维护列表B过大导致JVM内存占用。
  4. 为了对比前后列表差值,内存开启频繁,gc频繁。
    在这里插入图片描述
    上面找出C并且告知客户端C停止上报,找出新增的D并告知客户端D重新上报数据。
    两个List找出差异这并不难,用JAVA里面HashSet的remove api 或者jdk 8的stream实现。

方案2:redis

  1. 上报数据直接写到zset,客户端唯一标识为member, score为上报的数据(如客户端等级)实时动态更新列表顺序。
  2. 定时取出zset score为top N的客户端,刷新到本地列表LIST。
  3. 比较取出前后列表的差值。

优点:

  1. redis读写纯内存操作,性能较好。
  2. zset每次add即可,无需判断是否存在,并且纯天然支持排序。

代码演示:

redisTemplate.opsForZSet().add(key, value, scoreVal)

key自定义,value必须唯一,如客户端ID,scoreVal表示客户端的排序值,如客户端等级。这样每次上报数据都实时更新,动态实现排序。
下面取出top N的数据:

redisTemplate.opsForZSet().reverseRangeWithScores(key, 0, N - 1);

取出top N后,要比较差集,这个可以用java api实现,但是这里可以借助redis的set方法difference来实现。
首先上面得到 top N集合后,写入到一个临时的集合Set中,假如它的key为tmpKey:

redisTemplate.opsForSet().add(tmpKey, top N集合)

假如旧的集合为key,它也是set结构的,那么要得到上面要被移除的C客户端,可以这样:

redisTemplate.opsForSet().difference(key, tmpKey)

要得到上面新加入的 D客户端,可以这样:

redisTemplate.opsForSet().difference(tmpKey, key)

最终将旧的集合key换为最新的top N的数据,实现如下:

#先从key集合中移除掉C
redisTemplate.opsForSet().remove(key, 被移除的C)  //这里可批量删,value支持数组
#再对key和tmpKey集合元素合并写到key中,即
redisTemplate.opsForSet().unionAndStore(key, tmpKey, key)

这时集合key中的元素就是最新的: A ,B, D
下一个定时任务周期,继续以上操作。。。
注意:上面临时tmpKey每次循环之前都要清空,最好再设置个过期时间,以免一直占用redis内存。

这里还有一个问题:
上面用zset存储的value必须为唯一对象,一般都是ID之类的,但是我们会有这样的需求:不仅仅只有ID,可能还有名称、当前等级这些附加属性。 这时怎么办,不可能把整个对象放到zset的value中,因为这样每次都是不同的value了,做不到更新score使其动态排序。

怎么办?
我们可以再利用redis的另外一个数据结构hash来帮我们实现,hash结构专门负责存储附加属性的值。zset的value依然是id,当我们要去拿这个id的附加属性值,可以去hash这边拿回来,接着做后续的处理逻辑。
这个hash结构我们可以这样设计:
hashKey: 自定义
key: id 对应zset的 value
value:附加属性的各种信息
在这里插入图片描述

### 如何使用 Java 和 Redis 实现排行榜的实时更新 为了实现实时更新的排行榜功能,可以充分利用 Redis 的 `ZSet` 数据结构。以下是详细的实现方法以及示例代码。 #### 使用的技术栈 - **Jedis 或 Lettuce**:作为 Redis 的 Java 客户端库[^1]。 - **Spring Data Redis**:提供更高层次的抽象,简化 Redis 操作。 - **Redis ZSet**:有序集合,能够按分数排序并支持高效的操作[^2]。 --- #### 核心逻辑说明 1. **数据存储** 利用 Redis 中的 `ZSet` 存储排名信息,其中成员表示用户 ID 或名称,分值表示用户的得分。 2. **实时更新** 当某个用户的得分发生变化时,通过 `zadd` 或 `zincrby` 更新其在 `ZSet` 中的分值。 3. **查询排行** 使用 `reverseRangeWithScores` 查询指定范围内的排名及其对应分值。 4. **定时清理过期数据** 对于日榜、周榜等场景,需定期清除旧的数据以保持性能和准确性。 --- #### 示例代码 ##### 1. 添加依赖 如果使用 Maven 构建项目,则需要引入以下依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> ``` ##### 2. 初始化 Redis 配置 配置 Redis 连接池及模板类: ```java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.StringRedisTemplate; @Configuration public class RedisConfig { @Bean public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) { return new StringRedisTemplate(redisConnectionFactory); } } ``` ##### 3. 实现排行榜服务 定义一个服务类用于管理排行榜的增删查改操作: ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.ZSetOperations; import org.springframework.stereotype.Service; import java.util.List; import java.util.Set; @Service public class RankingService { private static final String RANKING_KEY = "ranking:global"; // 全局排行榜 Key @Autowired private ZSetOperations<String, String> zSetOperations; /** * 更新用户的得分 * * @param userId 用户ID * @param score 得分增量 */ public void updateScore(String userId, double score) { zSetOperations.incrementScore(RANKING_KEY, userId, score); // 使用 zincrby 更新分值 } /** * 获取前 N 名用户 * * @param topN 前几名 * @return 排名列表 */ public List<Object[]> getTopN(int topN) { Set<ZSetOperations.TypedTuple<String>> tuples = zSetOperations.reverseRangeWithScores(RANKING_KEY, 0, topN - 1); return tuples.stream() .map(tuple -> new Object[]{tuple.getValue(), tuple.getScore()}) .toList(); } } ``` ##### 4. 测试代码 模拟用户行为并查看排行榜变化: ```java import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest public class RankingServiceTest { @Autowired private RankingService rankingService; @Test public void testRankingUpdateAndQuery() { // 更新用户得分 rankingService.updateScore("user1", 100); // user1 获得 100 分 rankingService.updateScore("user2", 200); // user2 获得 200 分 rankingService.updateScore("user1", 50); // user1 再获得 50 分 (总分变为 150) // 查看前两名 List<Object[]> topUsers = rankingService.getTopN(2); for (Object[] user : topUsers) { System.out.println("User: " + user[0] + ", Score: " + user[1]); } } } ``` --- #### 日志输出样例 假设运行测试代码后,控制台会打印如下内容: ``` User: user2, Score: 200.0 User: user1, Score: 150.0 ``` --- #### 性能优化建议 1. **批量处理** 如果有大量用户同时更新得分,考虑将请求暂存在本地队列中,并异步写入 Redis[^4]。 2. **分布式锁** 在多实例部署的情况下,可能需要借助 Redisson 提供的分布式锁来防止竞争条件。 3. **分区策略** 对于超大规模排行榜,可通过哈希算法对不同区间的用户进行分区存储,减少单个 Key 的负载。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值