Redis Zset詳解:排行榜絕佳選擇
最近我們發(fā)布了一款新的app,其中包含一個搜索功能。在搜索時,會給用戶展示四個熱門搜索詞匯。我們利用 Redis 的有序集合(zset)實現(xiàn)了這一功能。由于應(yīng)用程序剛剛上線并且尚未大力推廣,所以熱門搜索詞匯顯示的是我們隨手測試詞匯,如測試、test、111等。這會給人一種不夠?qū)I(yè)的印象。為了提升產(chǎn)品形象,我們計劃通過后臺刪除這些測試的詞匯,使熱門搜索詞匯更加貼近實際使用情況。今天,我將與大家分享在 Redis 命令行中操作有序集合(zset)的命令,以及我們實現(xiàn)熱門搜索詞匯功能的思路。
Redis ZSET 詳解
Redis 中的 ZSET(有序集合)是一種有序的數(shù)據(jù)結(jié)構(gòu),它類似于 SET(集合),但每個成員都關(guān)聯(lián)著一個分?jǐn)?shù)(score),通過分?jǐn)?shù)來進行排序。這使得 ZSET 既可以像 SET 一樣快速查找成員,又可以按照分?jǐn)?shù)從小到大或從大到小進行排序。
ZSET 的特點包括:
- 有序性:成員按照分?jǐn)?shù)的順序排列,可以進行范圍查詢和排名操作。
- 唯一性:每個成員都是唯一的,但不同成員可以有相同的分?jǐn)?shù)。
- 快速查找:和 SET 類似,ZSET 也可以在 O(1) 的時間復(fù)雜度內(nèi)查找單個成員。
- 分?jǐn)?shù)(score)更新:可以對成員的分?jǐn)?shù)進行增加或減少操作,同時保持排序。
ZSET 的底層實現(xiàn)會根據(jù)實際的情況選擇ziplist(壓縮列表)/listpack(緊湊列表)(redis7.0已經(jīng)將 listpack 完整替代 ziplis) 或者skiplist(跳躍表),Redis 會根據(jù)實際情況動態(tài)地在這兩種底層結(jié)構(gòu)之間切換,使得其在內(nèi)存和性能之間平衡。這是由兩個配置參數(shù):zset-max-ziplist-entries 和 zset-max-ziplist-value控制的,其默認(rèn)值為128和64。當(dāng) Zset 存儲的元素數(shù)量超過zset-max-ziplist-entries的值或者最長元素的長度超過 zset-max-ziplist-value的值的時候Redis 會將底層結(jié)構(gòu)從壓縮列表/緊湊列表轉(zhuǎn)換為跳躍表。壓縮列表/緊湊列表占用的內(nèi)存比較少,但是修改數(shù)據(jù)時可能會對整個列表進行重寫,性能較低; 跳躍表的查找和修改數(shù)據(jù)的性能較高,但是占用的內(nèi)存也較多。
我們在redis 命令行中可以通過以下命令查看 zset的配置參數(shù):
config get zset*
圖片
Redis ZSET 使用場景
- 排行榜
Redis 的zset是設(shè)計實時排行的絕佳選擇,我們可以使用它來完成各種排行榜、熱門詞匯等場景的實現(xiàn)。我們app的熱搜詞匯也是通過zset實現(xiàn)的,本文中也將介紹熱搜詞匯的實現(xiàn)方式。
- 延時隊列
我們可以將時間戳設(shè)置為zset的score,延時處理的任務(wù)作為元素,定期或者循環(huán)掃描zset來處理到達時間的任務(wù)。
- 滑動窗口限流
我們可以將接口地址設(shè)置為zset的key,時間戳設(shè)置為zset的score,使用uuid作為元素,那么我們可以通過zset獲取到 score固定窗口范圍的時間內(nèi)的請求數(shù)來達到限流的目的。
REDISSON 操作ZSET數(shù)據(jù)
代碼如下:
package cn.xj.xjdoc.redis.zset;
import jakarta.annotation.Resource;
import org.redisson.api.RScoredSortedSet;
import org.redisson.api.RedissonClient;
import org.redisson.client.protocol.ScoredEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.Collection;
@Service
public class ZSETService {
private static final Logger log = LoggerFactory.getLogger(ZSETService.class);
@Resource
private RedissonClient redissonClient;
public void operation(){
String zsetKey = "xjzset";
RScoredSortedSet<String> zset = redissonClient.getScoredSortedSet(zsetKey);
//添加元素
zset.add(1.0, "修己xj1");
zset.add(2.0, "修己xj2");
zset.add(3.0, "修己xj3");
zset.add(4.0, "修己xj4");
// 獲取ZSET中指定成員的分?jǐn)?shù)
Double score = zset.getScore("修己xj2");
log.info("1、獲取ZSET中指定成員的分?jǐn)?shù):{}",score);
//獲取ZSET中指定成員的排名(分?jǐn)?shù)從小到大排序)
Integer rank = zset.rank("修己xj3");
log.info("2、獲取ZSET中指定成員的排名(分?jǐn)?shù)從小到大排序):{}",rank);
//獲取ZSET中指定成員的排名(分?jǐn)?shù)從大到小排序)
Integer reverseRank = zset.revRank("修己xj4");
log.info("3、獲取ZSET中指定成員的排名(分?jǐn)?shù)從大到小排序):{}",reverseRank);
// 獲取ZSET中指定排名范圍內(nèi)的成員(分?jǐn)?shù)從小到大排序)
Collection<String> membersInRange = zset.valueRange(0, 1);
membersInRange.forEach(o->log.info("4、獲取ZSET中指定排名范圍內(nèi)的成員(分?jǐn)?shù)從小到大排序):{}",o));
// 獲取ZSET中指定排名范圍內(nèi)的成員(分?jǐn)?shù)從大到小排序)
Collection<String> membersInRangeRever = zset.valueRangeReversed(0, 1);
membersInRangeRever.forEach(o->log.info("5、獲取ZSET中指定排名范圍內(nèi)的成員(分?jǐn)?shù)從大到小排序):{}",o));
//獲取ZSET中指定分?jǐn)?shù)范圍內(nèi)的成員(分?jǐn)?shù)從小到大排序)
Collection<String> membersInScoreRange = zset.valueRange(2.0, true, 3.0, true);
membersInScoreRange.forEach(o->log.info("6、獲取ZSET中指定分?jǐn)?shù)范圍內(nèi)的成員(分?jǐn)?shù)從小到大排序):{}",o));
//獲取ZSET中指定分?jǐn)?shù)范圍內(nèi)的成員(分?jǐn)?shù)從大到小排序)
Collection<String> membersInScoreRever = zset.valueRangeReversed(2.0, true, 3.0, true);
membersInScoreRever.forEach(o->log.info("7、獲取ZSET中指定分?jǐn)?shù)范圍內(nèi)的成員(分?jǐn)?shù)從大到小排序):{}",o));
//獲取ZSET中指定排名范圍內(nèi)的成員及其分?jǐn)?shù)
Collection<ScoredEntry<String>> membersWithScoresInRange = zset.entryRange(0, 1);
membersWithScoresInRange.forEach(o->log.info("8、獲取ZSET中指定排名范圍內(nèi)的成員及其分?jǐn)?shù),成員:{},分?jǐn)?shù)",o.getValue(),o.getScore()));
//獲取ZSET中指定分?jǐn)?shù)范圍內(nèi)的成員及其分?jǐn)?shù)
Collection<ScoredEntry<String>> membersWithScoresInScoreRange = zset.entryRange(3.0, true, 4.0, true);
membersWithScoresInScoreRange.forEach(o->log.info("9、獲取ZSET中指定分?jǐn)?shù)范圍內(nèi)的成員及其分?jǐn)?shù),成員:{},分?jǐn)?shù)",o.getValue(),o.getScore()));
//
Double newScore = zset.addScore("修己xj4", 1);
log.info("10、增加1之后指定成員的分?jǐn)?shù):{}",newScore);
//刪除ZSET 中的指定成員
Boolean removedFlag = zset.remove("修己xj3");
log.info("11、刪除ZSET 中的指定成員:{}",removedFlag);
//刪除指定排名范圍內(nèi)的成員
Integer removedByRangeCount = zset.removeRangeByRank(0, 1);
log.info("12、刪除指定排名范圍內(nèi)的成員數(shù)量:{}",removedByRangeCount);
//刪除指定分?jǐn)?shù)范圍內(nèi)的成員
Integer removedByScoreCount = zset.removeRangeByScore(3.0, true, 4.0, true);
log.info("13、刪除指定分?jǐn)?shù)范圍內(nèi)的成員數(shù)量:{}",removedByScoreCount);
}
}
執(zhí)行結(jié)果如下:
1、獲取ZSET中指定成員的分?jǐn)?shù):2.0
2、獲取ZSET中指定成員的排名(分?jǐn)?shù)從小到大排序):2
3、獲取ZSET中指定成員的排名(分?jǐn)?shù)從大到小排序):0
4、獲取ZSET中指定排名范圍內(nèi)的成員(分?jǐn)?shù)從小到大排序):修己xj1
4、獲取ZSET中指定排名范圍內(nèi)的成員(分?jǐn)?shù)從小到大排序):修己xj2
5、獲取ZSET中指定排名范圍內(nèi)的成員(分?jǐn)?shù)從大到小排序):修己xj4
5、獲取ZSET中指定排名范圍內(nèi)的成員(分?jǐn)?shù)從大到小排序):修己xj3
6、獲取ZSET中指定分?jǐn)?shù)范圍內(nèi)的成員(分?jǐn)?shù)從小到大排序):修己xj2
6、獲取ZSET中指定分?jǐn)?shù)范圍內(nèi)的成員(分?jǐn)?shù)從小到大排序):修己xj3
7、獲取ZSET中指定分?jǐn)?shù)范圍內(nèi)的成員(分?jǐn)?shù)從大到小排序):修己xj3
7、獲取ZSET中指定分?jǐn)?shù)范圍內(nèi)的成員(分?jǐn)?shù)從大到小排序):修己xj2
8、獲取ZSET中指定排名范圍內(nèi)的成員及其分?jǐn)?shù),成員:修己xj1,分?jǐn)?shù)
8、獲取ZSET中指定排名范圍內(nèi)的成員及其分?jǐn)?shù),成員:修己xj2,分?jǐn)?shù)
9、獲取ZSET中指定分?jǐn)?shù)范圍內(nèi)的成員及其分?jǐn)?shù),成員:修己xj3,分?jǐn)?shù)
9、獲取ZSET中指定分?jǐn)?shù)范圍內(nèi)的成員及其分?jǐn)?shù),成員:修己xj4,分?jǐn)?shù)
10、增加1之后指定成員的分?jǐn)?shù):5.0
11、刪除ZSET 中的指定成員:true
12、刪除指定排名范圍內(nèi)的成員數(shù)量:2
13、刪除指定分?jǐn)?shù)范圍內(nèi)的成員數(shù)量:0
命令行操作ZSET數(shù)據(jù)
- zadd 添加成員
zadd xjzset 1 "修己xj1" 2 "修己xj2" 3 "修己xj3" 4 "修己xj4"
- zscore 獲取指定成員的分?jǐn)?shù)
zscore xjzset '修己xj2'
- zrank 獲取指定成員的排名(分?jǐn)?shù)從小到大排序)
zrank xjzset 修己xj3
- zrevrank 獲取指定成員的排名(分?jǐn)?shù)從大到小排序)
zrevrank xjzset 修己xj3
- zrange/zrevrange 獲取ZSET中指定排名范圍內(nèi)的成員 zrange:分?jǐn)?shù)從小到大排序,我們加了一些測試數(shù)據(jù),如下
圖片
zrevrange:分?jǐn)?shù)從大到小排序
圖片
zrange key start stop [withscores]
zrevrange key start stop [withscores]
其中,key是zset的鍵名,start是起始索引,stop結(jié)束索引,withscores表示是否同時返回分?jǐn)?shù)??梢允褂秘?fù)數(shù)索引表示從末尾開始,比如-1表示最后一個元素。zrange key 0 -1 則會顯示出所有元素
zrange xjzset 1 2 withscores
- zrangebyscore/zrevrangebyscore 獲取ZSET中指定分?jǐn)?shù)score范圍內(nèi)的成員 zrangebyscore:分?jǐn)?shù)從小到大排序,zrevrangebyscore:分?jǐn)?shù)從大到小排序
zrangebyscore key min max [withscores]
zrevrangebyscore key max min [withscores]
其中,key是zset的鍵名,min 和 max 表示score的范圍,范圍為閉區(qū)間,withscores表示是否同時返回分?jǐn)?shù)。
zrangebyscore xjzset 2.5 3.5 withscores
- zincrby 將指定成員的分?jǐn)?shù)增加指定的值
zincrby xjzset 1 修己xj3
注: 進行double的值的運算時可能會丟失精度,如果對score進行運算時盡可能使用整數(shù)運算。
圖片
- zcard 返回zset中成員的數(shù)量
zcard xjzset
- zcount 獲取指定范圍分?jǐn)?shù)內(nèi)的成員的數(shù)量
zcount key min max
其中,key是zset的鍵名,min 和 max 表示score的范圍,范圍為閉區(qū)間。
zcount xjzset 0 2
- zrem 刪除指定成員
zrem xjzset
- zremrangebyrank 刪除指定排名范圍內(nèi)的成員
zremrangebyrank key start stop
其中,key是zset的鍵名,start是起始索引,stop結(jié)束索引。
zremrangebyrank xjzset 1 1
- zremrangebyscore 刪除指定分?jǐn)?shù)范圍內(nèi)的成員
zremrangebyscore key min max
其中,key是zset的鍵名,min 和 max 表示score的范圍,范圍為閉區(qū)間。
zremrangebyscore xjzset 0 3
圖片
熱搜詞匯功能實現(xiàn)
我們設(shè)計思路是 將每個搜索詞作為有序集合的成員,而搜索次數(shù)作為成員的分?jǐn)?shù),每次搜索的時候?qū)@個搜索詞的分?jǐn)?shù)加1,這樣可以根據(jù)搜索次數(shù)對熱搜詞進行排序。
- 搜索接口
public String keySearch(String keyStr){
String hotSearchKey = "xj_hotSearch";
RScoredSortedSet<String> hotSearchZSet = redissonClient.getScoredSortedSet(hotSearchKey);
//更新zset中當(dāng)前搜索詞的搜索次數(shù)
hotSearchZSet.addScore(keyStr,1);
//搜索邏輯
//doSearch(keyStr);
return keyStr;
}
- 熱搜詞匯查詢接口
public Collection<String> hotSearch(){
String hotSearchKey = "xj_hotSearch";
RScoredSortedSet<String> hotSearchZSet = redissonClient.getScoredSortedSet(hotSearchKey);
//獲取zset中點擊次數(shù)排名前5的數(shù)據(jù)
Collection<String> hotList= hotSearchZSet.valueRangeReversed(0,4);
return hotList;
}
我們加了一些測試數(shù)據(jù),如下
圖片
圖片
總結(jié)
通過本文的介紹,你學(xué)會了如何利用Spring Boot和Redis的ZSET數(shù)據(jù)結(jié)構(gòu)實現(xiàn)熱門搜索功能,并深入了解了熱搜詞匯的實現(xiàn)細(xì)節(jié)。通過合理的設(shè)計和優(yōu)化,可以為用戶提供更好的搜索體驗,同時也提升了應(yīng)用程序的性能和可擴展性。