Redis中的有序集合zset從使用到原理分析
開篇:排行榜背后的秘密
想象一下你正在玩一個(gè)手機(jī)游戲,游戲里有一個(gè)全球排行榜,實(shí)時(shí)顯示著所有玩家的得分情況。這個(gè)排行榜每分鐘都在變化,新玩家加入,老玩家提升分?jǐn)?shù),排名不斷調(diào)整。這種場(chǎng)景下,如果使用傳統(tǒng)的關(guān)系型數(shù)據(jù)庫來實(shí)現(xiàn),每次更新分?jǐn)?shù)都需要重新排序整個(gè)表,性能將會(huì)非常糟糕。
這就像在高峰期的地鐵站,如果每次有人進(jìn)出站都需要重新排隊(duì),那場(chǎng)面一定會(huì)混亂不堪。而Redis的有序集合(zset)就像是一個(gè)智能的排隊(duì)系統(tǒng),它能自動(dòng)維護(hù)元素的順序,無論新增、刪除還是修改元素,都能高效地保持有序狀態(tài)。
今天我們就來深入探討Redis中這個(gè)強(qiáng)大的數(shù)據(jù)結(jié)構(gòu)——有序集合(zset),從基本使用到內(nèi)部實(shí)現(xiàn)原理,幫助大家更好地理解和運(yùn)用這個(gè)工具。
小知識(shí): Redis的有序集合(zset)是字符串成員(member)與浮點(diǎn)數(shù)分值(score)的有序映射,集合中的成員是唯一的,但分值可以重復(fù)。
一、zset的基本使用
理解了zset的應(yīng)用場(chǎng)景后,我們來看看如何使用它。Redis為zset提供了豐富的命令集,讓我們能夠方便地操作這個(gè)數(shù)據(jù)結(jié)構(gòu)。
1.1 常用命令
下面是一些最常用的zset命令:
# 添加元素 ZADD key score member [score member ...] # 獲取元素分?jǐn)?shù) ZSCORE key member # 獲取元素排名(從低到高) ZRANK key member # 獲取元素排名(從高到低) ZREVRANK key member # 獲取范圍內(nèi)的元素(按分?jǐn)?shù)從低到高) ZRANGE key start stop [WITHSCORES] # 獲取范圍內(nèi)的元素(按分?jǐn)?shù)從高到低) ZREVRANGE key start stop [WITHSCORES] # 獲取分?jǐn)?shù)范圍內(nèi)的元素 ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] # 刪除元素 ZREM key member [member ...] # 獲取集合大小 ZCARD key # 統(tǒng)計(jì)分?jǐn)?shù)范圍內(nèi)的元素?cái)?shù)量 ZCOUNT key min max # 增加元素的分?jǐn)?shù) ZINCRBY key increment member
這些命令構(gòu)成了zset的基本操作集,能夠滿足大多數(shù)使用場(chǎng)景的需求。
1.2 Java客戶端示例
在實(shí)際開發(fā)中,我們通常會(huì)使用Redis的Java客戶端來操作zset。下面是一個(gè)使用Jedis的示例:
import redis.clients.jedis.Jedis;
import java.util.Set;
public class ZSetExample {
public static void main(String[] args) {
// 連接Redis
Jedis jedis = new Jedis("localhost");
// 添加元素到zset
jedis.zadd("player_scores", 100, "player1");
jedis.zadd("player_scores", 200, "player2");
jedis.zadd("player_scores", 150, "player3");
// 獲取所有元素(按分?jǐn)?shù)升序)
Set<String> players = jedis.zrange("player_scores", 0, -1);
System.out.println("所有玩家(升序): " + players);
// 獲取玩家排名
Long rank = jedis.zrank("player_scores", "player2");
System.out.println("player2的排名: " + (rank + 1));
// 獲取玩家分?jǐn)?shù)
Double score = jedis.zscore("player_scores", "player3");
System.out.println("player3的分?jǐn)?shù): " + score);
// 增加玩家分?jǐn)?shù)
jedis.zincrby("player_scores", 50, "player1");
// 關(guān)閉連接
jedis.close();
}
}
上述代碼展示了如何使用Jedis客戶端操作zset。我們首先添加了幾個(gè)玩家的分?jǐn)?shù),然后查詢了排序結(jié)果、特定玩家的排名和分?jǐn)?shù),最后還演示了如何增加玩家的分?jǐn)?shù)。
最佳實(shí)踐: 在實(shí)際項(xiàng)目中,建議使用連接池來管理Redis連接,而不是每次操作都創(chuàng)建新連接。這樣可以顯著提高性能。
二、zset的應(yīng)用場(chǎng)景
掌握了基本操作后,我們來看看zset在實(shí)際項(xiàng)目中的典型應(yīng)用場(chǎng)景。zset的獨(dú)特特性使其在某些場(chǎng)景下成為不可替代的解決方案。
2.1 排行榜系統(tǒng)
這是zset最經(jīng)典的應(yīng)用場(chǎng)景。無論是游戲玩家排名、商品銷量排行,還是熱門內(nèi)容推薦,zset都能輕松應(yīng)對(duì)。
// 更新玩家分?jǐn)?shù)
public void updatePlayerScore(String playerId, double score) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.zadd("game_leaderboard", score, playerId);
}
}
// 獲取前10名玩家
public List<String> getTop10Players() {
try (Jedis jedis = jedisPool.getResource()) {
return new ArrayList<>(jedis.zrevrange("game_leaderboard", 0, 9));
}
}
上述代碼展示了如何實(shí)現(xiàn)一個(gè)簡(jiǎn)單的游戲排行榜系統(tǒng)。zadd命令會(huì)自動(dòng)維護(hù)元素的排序,而zrevrange可以方便地獲取排名靠前的元素。
2.2 延遲隊(duì)列
zset可以用作延遲隊(duì)列的實(shí)現(xiàn)基礎(chǔ)。將任務(wù)執(zhí)行時(shí)間作為score,使用當(dāng)前時(shí)間戳作為判斷依據(jù),可以輕松實(shí)現(xiàn)定時(shí)任務(wù)。
// 添加延遲任務(wù)
public void addDelayedTask(String taskId, long delaySeconds) {
try (Jedis jedis = jedisPool.getResource()) {
long executeTime = System.currentTimeMillis() + delaySeconds * 1000;
jedis.zadd("delayed_tasks", executeTime, taskId);
}
}
// 處理到期任務(wù)
public void processReadyTasks() {
try (Jedis jedis = jedisPool.getResource()) {
// 獲取所有score小于當(dāng)前時(shí)間的任務(wù)
Set<String> tasks = jedis.zrangeByScore("delayed_tasks", 0, System.currentTimeMillis());
for (String task : tasks) {
// 處理任務(wù)
handleTask(task);
// 從隊(duì)列中移除已處理任務(wù)
jedis.zrem("delayed_tasks", task);
}
}
}
這個(gè)例子展示了如何使用zset實(shí)現(xiàn)延遲隊(duì)列。通過將執(zhí)行時(shí)間作為score,我們可以輕松查詢到期的任務(wù)。
2.3 時(shí)間軸
社交網(wǎng)絡(luò)中的時(shí)間軸功能也可以使用zset來實(shí)現(xiàn)。將時(shí)間戳作為score,內(nèi)容ID作為member,可以方便地按時(shí)間順序獲取內(nèi)容。

以上流程圖說明了使用zset實(shí)現(xiàn)時(shí)間軸功能的基本流程。新內(nèi)容發(fā)布時(shí),將內(nèi)容ID和時(shí)間戳添加到zset中;查看時(shí)間軸時(shí),按時(shí)間倒序獲取最新的內(nèi)容。
三、zset的實(shí)現(xiàn)原理
了解了zset的應(yīng)用場(chǎng)景后,我們不禁要問:Redis是如何實(shí)現(xiàn)這個(gè)高效的數(shù)據(jù)結(jié)構(gòu)的?下面我們就來揭開zset的內(nèi)部實(shí)現(xiàn)原理。
3.1 數(shù)據(jù)結(jié)構(gòu)選擇
Redis的zset同時(shí)使用了兩種數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn):
- 跳躍表(Skip List):用于維護(hù)元素的有序性,支持快速的范圍查詢
- 哈希表(Hash Table):用于存儲(chǔ)member到score的映射,支持O(1)時(shí)間復(fù)雜度的分?jǐn)?shù)查詢

這個(gè)類圖展示了zset的內(nèi)部結(jié)構(gòu)。zset同時(shí)維護(hù)了一個(gè)哈希表和一個(gè)跳躍表,哈希表用于快速查找member對(duì)應(yīng)的score,跳躍表用于維護(hù)member的有序排列。
3.2 跳躍表詳解
跳躍表是zset實(shí)現(xiàn)有序性的核心數(shù)據(jù)結(jié)構(gòu)。它是一種概率平衡的數(shù)據(jù)結(jié)構(gòu),可以看作是多層鏈表的結(jié)合體。

這個(gè)流程圖展示了跳躍表的基本結(jié)構(gòu)和查找過程。跳躍表通過建立多級(jí)索引,使得查找時(shí)間復(fù)雜度可以降低到O(log n)。
3.3 為什么使用跳躍表
Redis選擇跳躍表而不是平衡樹來實(shí)現(xiàn)zset,主要基于以下幾個(gè)原因:
- 實(shí)現(xiàn)簡(jiǎn)單:跳躍表的實(shí)現(xiàn)比平衡樹簡(jiǎn)單得多,代碼更易于維護(hù)
- 范圍查詢高效:跳躍表在范圍查詢上比平衡樹更高效
- 并發(fā)友好:跳躍表在并發(fā)環(huán)境下更容易實(shí)現(xiàn)無鎖操作
- 內(nèi)存友好:跳躍表在某些情況下比平衡樹更節(jié)省內(nèi)存
3.4 內(nèi)存結(jié)構(gòu)示例
讓我們通過一個(gè)具體的例子來看看zset在內(nèi)存中的存儲(chǔ)方式。假設(shè)我們有以下zset:
ZADD myzset 10 "A" ZADD myzset 20 "B" ZADD myzset 15 "C"

這個(gè)狀態(tài)圖展示了上述zset在內(nèi)存中的存儲(chǔ)結(jié)構(gòu)。哈希表部分存儲(chǔ)了member到score的映射,跳躍表部分維護(hù)了member的有序排列。
四、zset的性能分析
了解了zset的實(shí)現(xiàn)原理后,我們來看看它的性能特點(diǎn),這對(duì)于我們?cè)趯?shí)際項(xiàng)目中選擇合適的解決方案非常重要。
4.1 時(shí)間復(fù)雜度
zset各操作的時(shí)間復(fù)雜度如下:
- ZADD:O(log n) - 需要更新跳躍表和哈希表
- ZREM:O(log n) - 需要從跳躍表和哈希表中刪除
- ZSCORE:O(1) - 直接從哈希表獲取
- ZRANK/ZREVRANK:O(log n) - 需要在跳躍表中查找
- ZRANGE/ZREVRANGE:O(log n + m) - m是返回的元素?cái)?shù)量
- ZCARD:O(1) - 直接返回集合大小
4.2 內(nèi)存占用
zset的內(nèi)存占用主要來自兩部分:
- 哈希表:存儲(chǔ)所有member和score的映射關(guān)系
- 跳躍表:存儲(chǔ)member的有序排列和各級(jí)索引
平均來說,zset的內(nèi)存占用大約是簡(jiǎn)單字符串的2-3倍。對(duì)于內(nèi)存敏感的應(yīng)用,需要謹(jǐn)慎使用大型zset。
注意: 當(dāng)zset的元素?cái)?shù)量較少時(shí)(默認(rèn)配置下小于128個(gè)元素),Redis會(huì)使用一種更緊湊的編碼方式(zip list)來存儲(chǔ)zset,可以顯著減少內(nèi)存使用。只有元素?cái)?shù)量超過閾值或元素大小超過限制時(shí),才會(huì)轉(zhuǎn)換為跳躍表+哈希表的存儲(chǔ)方式。
五、高級(jí)用法與優(yōu)化
掌握了zset的基本原理后,我們來看看一些高級(jí)用法和優(yōu)化技巧,這些可以幫助我們?cè)趯?shí)際項(xiàng)目中更好地利用zset。
5.1 聚合操作
Redis提供了ZUNIONSTORE和ZINTERSTORE命令,可以對(duì)多個(gè)zset進(jìn)行并集和交集運(yùn)算。
# 計(jì)算兩個(gè)zset的并集 ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX] # 計(jì)算兩個(gè)zset的交集 ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE SUM|MIN|MAX]
這些命令在需要合并多個(gè)排行榜或計(jì)算多個(gè)維度的交集時(shí)非常有用。
5.2 使用權(quán)重和聚合函數(shù)
在聚合操作中,我們可以為每個(gè)zset指定權(quán)重,并選擇不同的聚合函數(shù):
# 創(chuàng)建兩個(gè)zset ZADD zset1 1 "A" 2 "B" ZADD zset2 10 "A" 20 "B" # 計(jì)算加權(quán)并集(第一個(gè)zset權(quán)重為1,第二個(gè)為0.1) ZUNIONSTORE result 2 zset1 zset2 WEIGHTS 1 0.1 AGGREGATE SUM # 結(jié)果應(yīng)該是: "A"→2, "B"→4 ZRANGE result 0 -1 WITHSCORES
這個(gè)例子展示了如何使用權(quán)重和聚合函數(shù)。通過合理設(shè)置權(quán)重,我們可以實(shí)現(xiàn)復(fù)雜的分?jǐn)?shù)計(jì)算邏輯。
5.3 大zset的優(yōu)化
當(dāng)zset非常大時(shí)(包含數(shù)百萬元素),需要考慮以下優(yōu)化措施:
- 分片:將大zset拆分為多個(gè)小zset
- 定期清理:移除過期或不再需要的元素
- 使用SCAN代替全量查詢:對(duì)于大范圍查詢,使用ZSCAN避免阻塞
- 合理設(shè)置zset-max-ziplist-entries:根據(jù)實(shí)際情況調(diào)整內(nèi)存優(yōu)化閾值

這個(gè)用戶旅程圖展示了大zset的各種優(yōu)化策略及其重要性和相關(guān)責(zé)任人。不同的策略適用于不同的場(chǎng)景,需要根據(jù)實(shí)際情況選擇。
六、總結(jié)
通過今天的討論,我們對(duì)Redis的有序集合(zset)有了全面的了解。讓我們回顧一下本文的主要內(nèi)容:
- 基本使用:介紹了zset的常用命令和Java客戶端示例
- 應(yīng)用場(chǎng)景:探討了zset在排行榜、延遲隊(duì)列和時(shí)間軸等場(chǎng)景的應(yīng)用
- 實(shí)現(xiàn)原理:深入分析了zset的跳躍表+哈希表的內(nèi)部實(shí)現(xiàn)
- 性能分析:了解了zset的時(shí)間復(fù)雜度和內(nèi)存占用特點(diǎn)
- 高級(jí)用法:學(xué)習(xí)了聚合操作、權(quán)重設(shè)置和大zset優(yōu)化等高級(jí)技巧
Redis的zset是一個(gè)非常強(qiáng)大且靈活的數(shù)據(jù)結(jié)構(gòu),它在許多場(chǎng)景下都能提供高效的解決方案。希望通過本文的分享,能幫助大家更好地理解和運(yùn)用這個(gè)工具。
在實(shí)際項(xiàng)目中,建議大家根據(jù)具體需求選擇合適的實(shí)現(xiàn)方式,并注意性能優(yōu)化和內(nèi)存使用。如果有任何問題或想法,歡迎隨時(shí)交流討論!
最后建議:
使用zset時(shí),要特別注意member的大小。過大的member會(huì)顯著增加內(nèi)存使用,建議盡量使用較短的member(如ID而非完整內(nèi)容)。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
利用Redis實(shí)現(xiàn)訂單30分鐘自動(dòng)取消
本文主要介紹了利用Redis實(shí)現(xiàn)訂單30分鐘自動(dòng)取消,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06
MyBatis緩存和二級(jí)緩存整合Redis的解決方案
這篇文章主要介紹了MyBatis緩存和二級(jí)緩存整合Redis,將MyBatis緩存和二級(jí)緩存整合Redis,可以提高查詢效率,同時(shí)也能保證數(shù)據(jù)的可靠性和一致性,需要的朋友可以參考下2023-07-07
一文解決Redis后臺(tái)持久化失敗的問題:內(nèi)存不足導(dǎo)致fork失敗
Redis作為一個(gè)內(nèi)存數(shù)據(jù)庫,在執(zhí)行后臺(tái)持久化(例如 BGSAVE 命令時(shí))需要fork一個(gè)子進(jìn)程來生成數(shù)據(jù)庫快照(RDB 文件),在生產(chǎn)環(huán)境中,有時(shí)你可能會(huì)在Redis日志中遇到持久化失敗的問題,本文將詳細(xì)介紹該問題的原因以及如何通過調(diào)整內(nèi)核和Redis配置來解決此問題2025-07-07
?Redis 串行生成順序編碼的方法實(shí)現(xiàn)
本文主要介紹了?Redis 串行生成順序編碼的方法實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04
利用Redis進(jìn)行數(shù)據(jù)緩存的項(xiàng)目實(shí)踐
在實(shí)際的業(yè)務(wù)場(chǎng)景中,Redis 一般和其他數(shù)據(jù)庫搭配使用,用來減輕后端數(shù)據(jù)庫的壓力,本文就介紹了利用Redis進(jìn)行數(shù)據(jù)緩存的項(xiàng)目實(shí)踐,具有一定的參考價(jià)值,感興趣的可以了解一下2022-06-06
Redis源碼解析sds字符串實(shí)現(xiàn)示例
這篇文章主要為大家介紹了Redis源碼解析sds字符串實(shí)現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08

