SpringBoot整合Redisson实现高性能实时排行榜

 更新时间:2025年04月18日 10:11:57   作者:程序员总部  
在当今的互联网应用中,排行榜功能几乎无处不在,那么如何用Spring Boot和Redisson快速搭建一个高性能的实时排行榜呢,下面我们就来详细聊聊这个技术方案吧

在当今的互联网应用中,排行榜功能几乎无处不在!无论是电商平台的销量榜单、游戏中的玩家战力排行,还是社交媒体的热度榜单,实时、高效的排行榜系统都是提升用户体验的关键。那么问题来了:如何用Spring Boot和Redisson快速搭建一个高性能的实时排行榜?今天我们就来详细聊聊这个技术方案!

为什么选择Redisson

首先,Redisson是一个基于Redis的Java客户端,它不仅封装了Redis的基本操作,还提供了分布式锁、布隆过滤器、排行榜等高级功能。相比直接操作Redis,Redisson的API更符合Java开发者的习惯,而且性能优化得非常好。举个例子,它的RLexSortedSet和RScoredSortedSet可以直接用来实现排行榜,支持毫秒级的实时更新!

// 示例:初始化Redisson客户端
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);

// 获取一个带分数的有序集合
RScoredSortedSet<String> ranking = redisson.getScoredSortedSet("user_ranking");

Spring Boot如何整合Redisson

整合过程非常简单!只需要几步:

添加依赖:在pom.xml中加入Redisson的Spring Boot Starter。

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.16.8</version>
</dependency>

配置Redis连接:在application.yml中填写Redis的地址和密码。

spring:
  redis:
    host: 127.0.0.1
    port: 6379

注入RedissonClient:直接通过@Autowired就能使用Redisson的功能。

@Autowired
private RedissonClient redissonClient;

是不是很简单?但别急,这只是开始!接下来才是真正的核心逻辑。

实现实时排行榜的关键逻辑

排行榜的核心功能通常包括:

  • 更新分数:比如用户完成一笔订单后增加积分。
  • 获取排名:查询某个用户的当前排名。
  • 获取Top N:展示前100名的榜单。

用Redisson实现这些功能非常直观:

// 更新用户分数
ranking.addScore("user1", 100);

// 获取用户排名(从高到低)
int rank = ranking.rank("user1");

// 获取Top 10
Collection<String> top10 = ranking.valueRangeReversed(0, 9);

这里有个小技巧:Redisson的valueRangeReversed方法可以直接返回倒序结果,避免了手动排序的开销!

性能优化与实战坑点

在实际项目中,排行榜可能会面临高并发更新的问题。比如双十一期间,电商平台的销量榜单每秒钟要更新成千上万次!这时候就需要考虑以下几点:

  • 批量操作:Redisson支持管道(Pipeline)和批量命令,能显著减少网络开销。
  • 分布式锁:如果涉及复杂的计算(比如积分加权),可以用RLock避免并发问题。
  • 内存优化:Redis的zset默认用ziplist存储少量数据,但数据量大时会自动转为skiplist,这时候要注意内存占用。
// 使用管道批量更新
RBatch batch = redisson.createBatch();
batch.getScoredSortedSet("ranking").addScoreAsync("user1", 10);
batch.getScoredSortedSet("ranking").addScoreAsync("user2", 20);
batch.execute();

扩展:多维度排行榜

有时候我们需要多维度的排名,比如“周榜”和“总榜”并存。这时候可以用Redisson的RScoredSortedSet配合不同的Key来实现:

RScoredSortedSet<String> weeklyRanking = redisson.getScoredSortedSet("ranking:weekly");
RScoredSortedSet<String> totalRanking = redisson.getScoredSortedSet("ranking:total");

​​​​​​​// 每周清零周榜
weeklyRanking.clear();

结语

通过Spring Boot和Redisson,我们能够轻松实现一个高性能的实时排行榜系统。从基础的功能到性能优化,Redisson都提供了简洁而强大的API。如果你正在面临类似的需求,不妨动手试试吧!遇到问题也别慌,多查文档、多交流,技术成长就是这么一步步来的。

方法补充

SpringBoot+Redission实现排行榜功能

1.引入Redis和Redission依赖

<!-- redis -->  
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-data-redis</artifactId>  
</dependency>  
  
<!-- redisson -->  
<dependency>  
    <groupId>org.redisson</groupId>  
    <artifactId>redisson-spring-boot-starter</artifactId>  
    <version>3.20.1</version>  
</dependency>

2.application.yml配置

--- # redis配置  
spring:  
  redis:  
    # 地址  
    host: localhost  
    # 端口,默认为6379  
    port: 6379  
    # 数据库索引  
    database: 0  
    # 密码(如没有密码请注释掉)  
    # password:    # 连接超时时间  
    timeout: 10s  
    # 是否开启ssl  
    ssl: false  
  
--- # redisson配置  
redisson:  
  # redis key前缀  
  keyPrefix: ${spring.application.name}  
  # 线程池数量  
  threads: 4  
  # Netty线程池数量  
  nettyThreads: 8  
  # 单节点配置  
  singleServerConfig:  
    # 客户端名称  
    clientName: ${spring.application.name}  
    # 最小空闲连接数  
    connectionMinimumIdleSize: 8  
    # 连接池大小  
    connectionPoolSize: 32  
    # 连接空闲超时,单位:毫秒  
    idleConnectionTimeout: 10000  
    # 命令等待超时,单位:毫秒  
    timeout: 3000  
    # 发布和订阅连接池大小  
    subscriptionConnectionPoolSize: 50

3.Java代码

Constant

/**  
 * @author Baisu  
 * @classname RankingConstant  
 * @description 排行榜常量数据  
 * @since 2024/5/6  
 */
public class RankingConstant {  
  
    public static final Long BASIC_QUANTITY = 10000000000000L;  
  
    public static final Long MAXIMUM_TIME_TIMIT = 29991231235959L;  
}

Controller

import cn.hutool.core.date.DateTime;  
import cn.hutool.core.date.DateUtil;  
import com.ranking.demo.common.R;  
import com.ranking.demo.common.constant.RankingConstant;  
import com.ranking.demo.demain.RankingVo;  
import com.ranking.demo.utils.RankingUtil;  
import com.ranking.demo.utils.RedisKey;  
import com.ranking.demo.utils.RedisUtil;  
import org.redisson.client.protocol.ScoredEntry;  
import org.springframework.beans.factory.annotation.Value;  
import org.springframework.web.bind.annotation.*;  
  
import java.util.ArrayList;  
import java.util.Collection;  
import java.util.List;  
  
/**  
 * @author Baisu  
 * @since 2024/4/28  
 */
@RestController  
public class DemoRankingController {  
  
    @Value("${spring.application.name}")  
    private String applicationName;  
  
    /**  
     * 项目启动测试方法  
     *  
     * @return applicationName  
     */    
    @GetMapping("")  
    public String demo() {  
        return applicationName;  
    }  
  
    /**  
     * 生成测试数据  
     *  
     * @return ok  
     */    
    @GetMapping("/generate_test_data")  
    public R<Object> generateTestData() {  
        RedisUtil.addScoreByMember(RedisKey.getRankingDemoKey(), 1L, "10001");  
        RedisUtil.addScoreByMember(RedisKey.getRankingDemoKey(), 2L, "10002");  
        RedisUtil.addScoreByMember(RedisKey.getRankingDemoKey(), 3L, "10003");  
        RedisUtil.addScoreByMember(RedisKey.getRankingDemoKey(), 4L, "10004");  
        RedisUtil.addScoreByMember(RedisKey.getRankingDemoKey(), 5L, "10005");  
        return R.ok();  
    }  
  
    /**  
     * 获取排行榜数据  
     *  
     * @param top 数量  
     * @return 排行榜数据  
     */  
    @GetMapping("/get_ranking")  
    public R<Object> getRanking(@RequestParam("top") Integer top) {  
        Collection<ScoredEntry<Object>> ranking = RedisUtil.getRanking(RedisKey.getRankingDemoKey(), 0, top - 1);  
        if (ranking.size() == 0) {  
            return R.fail("暂无排行榜数据");  
        }  
        List<RankingVo> list = new ArrayList<>();  
        for (ScoredEntry<Object> entry : ranking) {  
            RankingVo vo = new RankingVo();  
            vo.setMember(entry.getValue().toString());  
            vo.setScore(RankingUtil.getScore(entry.getScore()));  
            vo.setTime(RankingUtil.getTimeStr(entry.getScore()));  
            list.add(vo);  
        }  
        return R.ok(list);  
    }  
  
    /**  
     * 增加成员分数值  
     *  
     * @param member 成员  
     * @return 是否增加成功  
     */  
    @GetMapping("/add_score_by_member")  
    public R<Object> addScoreByMember(@RequestParam("member") String member) {  
        Double scoreByMember = RedisUtil.getScoreByMember(RedisKey.getRankingDemoKey(), member);  
        if (scoreByMember == null) {  
            scoreByMember = 0.0;  
        }  
        RedisUtil.addScoreByMember(RedisKey.getRankingDemoKey(), RankingUtil.getScore(scoreByMember) + 1, member);  
        return R.ok();  
    }  
  
    /**  
     * 获取成员分数值  
     *  
     * @param member 成员  
     * @return 分数值  
     */  
    @GetMapping("/get_score_by_member")  
    public R<Object> getScoreByMember(@RequestParam("member") String member) {  
        Double scoreByMember = RedisUtil.getScoreByMember(RedisKey.getRankingDemoKey(), member);  
        if (scoreByMember == null) {  
            return R.fail("该成员不存在");  
        }  
        RankingVo vo = new RankingVo();  
        vo.setMember(member);  
        vo.setScore(RankingUtil.getScore(scoreByMember));  
        vo.setTime(RankingUtil.getTimeStr(scoreByMember));  
        return R.ok(vo);  
    }  
  
}

Domain

import lombok.Data;  
  
/**  
 * @author Baisu  
 * @classname RankingVo  
 * @description 排行榜展示类  
 * @since 2024/5/6  
 */
@Data  
public class RankingVo {  
  
    /**  
     * 成员  
     */  
    private String member;  
    /**  
     * 分数值  
     */  
    private Long score;  
    /**  
     * 时间  
     */  
    private String time;  
  
}

Utils

/**  
 * @author Baisu  
 * @classname RedisKey  
 * @description Redis索引  
 * @since 2024/5/6  
 */
public class RedisKey {  
  
    private static final String RANKING_DEMO_KEY = "ranking_demo";  
  
    public static String getRankingDemoKey() {  
        return RANKING_DEMO_KEY;  
    }  
}
import cn.hutool.core.date.DateTime;  
import cn.hutool.core.date.DateUtil;  
import cn.hutool.extra.spring.SpringUtil;  
import com.ranking.demo.common.constant.RankingConstant;  
import org.redisson.api.RScoredSortedSet;  
import org.redisson.api.RedissonClient;  
import org.redisson.client.protocol.ScoredEntry;  
  
import java.util.Collection;  
  
/**  
 * @author Baisu  
 * @classname RedisUtil  
 * @description Redis工具类  
 * @since 2024/5/6  
 */
public class RedisUtil {  
  
    private static final RedissonClient REDISSON_CLIENT = SpringUtil.getBean(RedissonClient.class);  
  
    /**  
     * 向有序集合中添加指定分数的成员  
     *  
     * @param key    有序集索引  
     * @param score  分数  
     * @param member 成员  
     * @return 是否成功  
     */  
    public static boolean addScoreByMember(String key, Long score, String member) {  
        RScoredSortedSet<String> rScoredSortedSet = REDISSON_CLIENT.getScoredSortedSet(key);  
        double v = score * RankingConstant.BASIC_QUANTITY + (RankingConstant.MAXIMUM_TIME_TIMIT - Long.parseLong(DateUtil.format(DateTime.now(), RankingUtil.FORMAT)));  
        return rScoredSortedSet.add(v, member);  
    }  
  
    /**  
     * 返回有序集中成员的分数值  
     *  
     * @param key    有序集索引  
     * @param member 成员  
     * @return 分数值(Double)  
     */    
     public static Double getScoreByMember(String key, String member) {  
        RScoredSortedSet<Object> scoredSortedSet = REDISSON_CLIENT.getScoredSortedSet(key);  
        return scoredSortedSet.getScore(member);  
    }  
  
    /**  
     * 返回有序集中指定位置的成员集合  
     *  
     * @param key   有序集索引  
     * @param start 开始索引  
     * @param end   结束索引  
     * @return 成员集合  
     */  
    public static Collection<ScoredEntry<Object>> getRanking(String key, int start, int end) {  
        RScoredSortedSet<Object> rScoredSortedSet = REDISSON_CLIENT.getScoredSortedSet(key);  
        return rScoredSortedSet.entryRangeReversed(start, end);  
    }  
  
}
import cn.hutool.core.date.DateUtil;  
import com.ranking.demo.common.constant.RankingConstant;  
  
/**  
 * @author Baisu  
 * @classname RankingUtil  
 * @description 排行榜工具类  
 * @since 2024/5/7  
 */
 public class RankingUtil {  
  
    public static final String FORMAT = "yyyyMMddHHmmss";  
  
    public static Long getScore(Double score) {  
        return Math.round(Math.floor(score / RankingConstant.BASIC_QUANTITY));  
    }  
  
    public static String getTimeStr(Double score) {  
        return String.valueOf(DateUtil.parse(String.valueOf(RankingConstant.MAXIMUM_TIME_TIMIT - Math.round(Math.floor(score)) % RankingConstant.BASIC_QUANTITY)));  
    }  
}

到此这篇关于SpringBoot整合Redisson实现高性能实时排行榜的文章就介绍到这了,更多相关SpringBoot Redisson实时排行榜内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • MyBatis-Plus与PageHelper依赖的jsqlparser库冲突

    MyBatis-Plus与PageHelper依赖的jsqlparser库冲突

    在升级SpringBoot到3.x版本的同时,升级MyBatis-Plus后发现PageHelper无法使用,原因是MyBatis-Plus和PageHelper都依赖jsqlparser库,且PageHelper要求特定版本的jsqlparser,解决方法是在项目中排除这两个库的jsqlparser依赖,直接引用jsqlparser4.7版本
    2024-10-10
  • Java 在volatile内部调用接口的方法

    Java 在volatile内部调用接口的方法

    在Java中,volatile 关键字通常用于确保变量的可见性和有序性,而不是用来修饰接口或方法调用的,这篇文章主要介绍了Java 在volatile内部调用接口的方法,需要的朋友可以参考下
    2024-07-07
  • java 中String.equals和==的比较

    java 中String.equals和==的比较

    这篇文章主要介绍了java 中String.equals和==的比较的相关资料,需要的朋友可以参考下
    2017-08-08
  • java.lang.NoClassDefFoundError错误的原因及解决方法

    java.lang.NoClassDefFoundError错误的原因及解决方法

    这篇文章主要给大家介绍了关于java.lang.NoClassDefFoundError错误的原因及解决的相关资料,java.lang.NoClassDefFoundError是Java虚拟机在运行时无法找到特定类的错误,需要的朋友可以参考下
    2023-10-10
  • Lucene fnm索引文件格式源码解析

    Lucene fnm索引文件格式源码解析

    这篇文章主要为大家介绍了Lucene fnm索引文件格式源码解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-03-03
  • Java实现的可选择及拖拽图片的面板功能【基于swing组件】

    Java实现的可选择及拖拽图片的面板功能【基于swing组件】

    这篇文章主要介绍了Java实现的可选择及拖拽图片的面板功能,涉及java基于swing组件选择与操作图片元素的相关实现技巧,需要的朋友可以参考下
    2018-01-01
  • Java中常用的代码汇总

    Java中常用的代码汇总

    本文给大家分享了20个常用的java代码,都是别人项目中使用过的代码,这里推荐给大家,有需要的小伙伴可以参考下。
    2015-05-05
  • 将内容写到txt文档里面并读取及删除的方法

    将内容写到txt文档里面并读取及删除的方法

    本文有个不错的示例,主要讲解如何将内容写到txt文档里面、读取文件里面的内容以及清除txt文件里面的内容
    2014-01-01
  • java使用MulticastSocket实现组播

    java使用MulticastSocket实现组播

    这篇文章主要为大家详细介绍了java使用MulticastSocket实现组播,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-01-01
  • 分享Java开发必须掌握的日志分析命令

    分享Java开发必须掌握的日志分析命令

    这篇文章主要介绍了分享Java开发必须掌握的日志分析命令,在日常工作中,如果我们遇到线上问题,一般的处理步骤应该是先保留现场,然后再考虑回滚,之后再是解决问题
    2019-07-07

最新评论