SpringBoot整合Redisson實(shí)現(xiàn)高性能實(shí)時(shí)排行榜
在當(dāng)今的互聯(lián)網(wǎng)應(yīng)用中,排行榜功能幾乎無(wú)處不在!無(wú)論是電商平臺(tái)的銷量榜單、游戲中的玩家戰(zhàn)力排行,還是社交媒體的熱度榜單,實(shí)時(shí)、高效的排行榜系統(tǒng)都是提升用戶體驗(yàn)的關(guān)鍵。那么問(wèn)題來(lái)了:如何用Spring Boot和Redisson快速搭建一個(gè)高性能的實(shí)時(shí)排行榜?今天我們就來(lái)詳細(xì)聊聊這個(gè)技術(shù)方案!
為什么選擇Redisson
首先,Redisson是一個(gè)基于Redis的Java客戶端,它不僅封裝了Redis的基本操作,還提供了分布式鎖、布隆過(guò)濾器、排行榜等高級(jí)功能。相比直接操作Redis,Redisson的API更符合Java開(kāi)發(fā)者的習(xí)慣,而且性能優(yōu)化得非常好。舉個(gè)例子,它的RLexSortedSet和RScoredSortedSet可以直接用來(lái)實(shí)現(xiàn)排行榜,支持毫秒級(jí)的實(shí)時(shí)更新!
// 示例:初始化Redisson客戶端
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
// 獲取一個(gè)帶分?jǐn)?shù)的有序集合
RScoredSortedSet<String> ranking = redisson.getScoredSortedSet("user_ranking");
Spring Boot如何整合Redisson
整合過(guò)程非常簡(jiǎn)單!只需要幾步:
添加依賴:在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:直接通過(guò)@Autowired就能使用Redisson的功能。
@Autowired private RedissonClient redissonClient;
是不是很簡(jiǎn)單?但別急,這只是開(kāi)始!接下來(lái)才是真正的核心邏輯。
實(shí)現(xiàn)實(shí)時(shí)排行榜的關(guān)鍵邏輯
排行榜的核心功能通常包括:
- 更新分?jǐn)?shù):比如用戶完成一筆訂單后增加積分。
- 獲取排名:查詢某個(gè)用戶的當(dāng)前排名。
- 獲取Top N:展示前100名的榜單。
用Redisson實(shí)現(xiàn)這些功能非常直觀:
// 更新用戶分?jǐn)?shù)
ranking.addScore("user1", 100);
// 獲取用戶排名(從高到低)
int rank = ranking.rank("user1");
// 獲取Top 10
Collection<String> top10 = ranking.valueRangeReversed(0, 9);
這里有個(gè)小技巧:Redisson的valueRangeReversed方法可以直接返回倒序結(jié)果,避免了手動(dòng)排序的開(kāi)銷!
性能優(yōu)化與實(shí)戰(zhàn)坑點(diǎn)
在實(shí)際項(xiàng)目中,排行榜可能會(huì)面臨高并發(fā)更新的問(wèn)題。比如雙十一期間,電商平臺(tái)的銷量榜單每秒鐘要更新成千上萬(wàn)次!這時(shí)候就需要考慮以下幾點(diǎn):
- 批量操作:Redisson支持管道(Pipeline)和批量命令,能顯著減少網(wǎng)絡(luò)開(kāi)銷。
- 分布式鎖:如果涉及復(fù)雜的計(jì)算(比如積分加權(quán)),可以用RLock避免并發(fā)問(wèn)題。
- 內(nèi)存優(yōu)化:Redis的zset默認(rèn)用ziplist存儲(chǔ)少量數(shù)據(jù),但數(shù)據(jù)量大時(shí)會(huì)自動(dòng)轉(zhuǎn)為skiplist,這時(shí)候要注意內(nèi)存占用。
// 使用管道批量更新
RBatch batch = redisson.createBatch();
batch.getScoredSortedSet("ranking").addScoreAsync("user1", 10);
batch.getScoredSortedSet("ranking").addScoreAsync("user2", 20);
batch.execute();
擴(kuò)展:多維度排行榜
有時(shí)候我們需要多維度的排名,比如“周榜”和“總榜”并存。這時(shí)候可以用Redisson的RScoredSortedSet配合不同的Key來(lái)實(shí)現(xiàn):
RScoredSortedSet<String> weeklyRanking = redisson.getScoredSortedSet("ranking:weekly");
RScoredSortedSet<String> totalRanking = redisson.getScoredSortedSet("ranking:total");
???????// 每周清零周榜
weeklyRanking.clear();
結(jié)語(yǔ)
通過(guò)Spring Boot和Redisson,我們能夠輕松實(shí)現(xiàn)一個(gè)高性能的實(shí)時(shí)排行榜系統(tǒng)。從基礎(chǔ)的功能到性能優(yōu)化,Redisson都提供了簡(jiǎn)潔而強(qiáng)大的API。如果你正在面臨類似的需求,不妨動(dòng)手試試吧!遇到問(wèn)題也別慌,多查文檔、多交流,技術(shù)成長(zhǎng)就是這么一步步來(lái)的。
方法補(bǔ)充
SpringBoot+Redission實(shí)現(xiàn)排行榜功能
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
# 端口,默認(rèn)為6379
port: 6379
# 數(shù)據(jù)庫(kù)索引
database: 0
# 密碼(如沒(méi)有密碼請(qǐng)注釋掉)
# password: # 連接超時(shí)時(shí)間
timeout: 10s
# 是否開(kāi)啟ssl
ssl: false
--- # redisson配置
redisson:
# redis key前綴
keyPrefix: ${spring.application.name}
# 線程池?cái)?shù)量
threads: 4
# Netty線程池?cái)?shù)量
nettyThreads: 8
# 單節(jié)點(diǎn)配置
singleServerConfig:
# 客戶端名稱
clientName: ${spring.application.name}
# 最小空閑連接數(shù)
connectionMinimumIdleSize: 8
# 連接池大小
connectionPoolSize: 32
# 連接空閑超時(shí),單位:毫秒
idleConnectionTimeout: 10000
# 命令等待超時(shí),單位:毫秒
timeout: 3000
# 發(fā)布和訂閱連接池大小
subscriptionConnectionPoolSize: 503.Java代碼
Constant
/**
* @author Baisu
* @classname RankingConstant
* @description 排行榜常量數(shù)據(jù)
* @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;
/**
* 項(xiàng)目啟動(dòng)測(cè)試方法
*
* @return applicationName
*/
@GetMapping("")
public String demo() {
return applicationName;
}
/**
* 生成測(cè)試數(shù)據(jù)
*
* @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();
}
/**
* 獲取排行榜數(shù)據(jù)
*
* @param top 數(shù)量
* @return 排行榜數(shù)據(jù)
*/
@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("暫無(wú)排行榜數(shù)據(jù)");
}
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);
}
/**
* 增加成員分?jǐn)?shù)值
*
* @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();
}
/**
* 獲取成員分?jǐn)?shù)值
*
* @param member 成員
* @return 分?jǐn)?shù)值
*/
@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;
/**
* 分?jǐn)?shù)值
*/
private Long score;
/**
* 時(shí)間
*/
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);
/**
* 向有序集合中添加指定分?jǐn)?shù)的成員
*
* @param key 有序集索引
* @param score 分?jǐn)?shù)
* @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);
}
/**
* 返回有序集中成員的分?jǐn)?shù)值
*
* @param key 有序集索引
* @param member 成員
* @return 分?jǐn)?shù)值(Double)
*/
public static Double getScoreByMember(String key, String member) {
RScoredSortedSet<Object> scoredSortedSet = REDISSON_CLIENT.getScoredSortedSet(key);
return scoredSortedSet.getScore(member);
}
/**
* 返回有序集中指定位置的成員集合
*
* @param key 有序集索引
* @param start 開(kāi)始索引
* @param end 結(jié)束索引
* @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)));
}
}
到此這篇關(guān)于SpringBoot整合Redisson實(shí)現(xiàn)高性能實(shí)時(shí)排行榜的文章就介紹到這了,更多相關(guān)SpringBoot Redisson實(shí)時(shí)排行榜內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
MyBatis-Plus與PageHelper依賴的jsqlparser庫(kù)沖突
在升級(jí)SpringBoot到3.x版本的同時(shí),升級(jí)MyBatis-Plus后發(fā)現(xiàn)PageHelper無(wú)法使用,原因是MyBatis-Plus和PageHelper都依賴jsqlparser庫(kù),且PageHelper要求特定版本的jsqlparser,解決方法是在項(xiàng)目中排除這兩個(gè)庫(kù)的jsqlparser依賴,直接引用jsqlparser4.7版本2024-10-10
Java 在volatile內(nèi)部調(diào)用接口的方法
在Java中,volatile?關(guān)鍵字通常用于確保變量的可見(jiàn)性和有序性,而不是用來(lái)修飾接口或方法調(diào)用的,這篇文章主要介紹了Java 在volatile內(nèi)部調(diào)用接口的方法,需要的朋友可以參考下2024-07-07
java.lang.NoClassDefFoundError錯(cuò)誤的原因及解決方法
這篇文章主要給大家介紹了關(guān)于java.lang.NoClassDefFoundError錯(cuò)誤的原因及解決的相關(guān)資料,java.lang.NoClassDefFoundError是Java虛擬機(jī)在運(yùn)行時(shí)無(wú)法找到特定類的錯(cuò)誤,需要的朋友可以參考下2023-10-10
Java實(shí)現(xiàn)的可選擇及拖拽圖片的面板功能【基于swing組件】
這篇文章主要介紹了Java實(shí)現(xiàn)的可選擇及拖拽圖片的面板功能,涉及java基于swing組件選擇與操作圖片元素的相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2018-01-01
java使用MulticastSocket實(shí)現(xiàn)組播
這篇文章主要為大家詳細(xì)介紹了java使用MulticastSocket實(shí)現(xiàn)組播,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-01-01

