Redis中ZSet數(shù)據(jù)結(jié)構(gòu)與滑動窗口應(yīng)用實現(xiàn)
Redis 的 ZSET(有序集合) 是一種結(jié)合了 哈希表 和 跳躍表(Skip List) 的混合數(shù)據(jù)結(jié)構(gòu),既能實現(xiàn) O(1) 復(fù)雜度的成員存在性判斷,又能以 O(logN) 復(fù)雜度維護(hù)有序性。
Redis ZSET 數(shù)據(jù)存儲機(jī)制
ZSET 有兩種實現(xiàn)機(jī)制:
SkipList + HashTable
數(shù)據(jù)實際上是同時存在于兩個數(shù)據(jù)結(jié)構(gòu)中的
跳表(SkipList)
- 按
score
排序存儲member
- 支持范圍查詢(ZRANGE 等命令)
- 維護(hù)成員的有序性
哈希表(HashTable)
- 存儲
member -> score
的映射 - 用于快速判斷成員是否存在(O(1) 復(fù)雜度)
- 直接獲取成員的分?jǐn)?shù)(ZSCORE 命令)
ZipList
ZipList:對于小型有序集合(元素少且 member ?。?,Redis 會使用 ziplist 編碼來節(jié)省內(nèi)存,只有當(dāng)元素數(shù)量或大小超過閾值時才會轉(zhuǎn)換為真正的跳躍表+哈希表實現(xiàn)。
- 按
(元素, score)
對順序存儲
數(shù)據(jù)一致性
Redis 通過保證所有寫操作(ZADD/ZREM等)都同時更新這兩個數(shù)據(jù)結(jié)構(gòu)來維護(hù)一致性。當(dāng)添加一個新成員時:
- 會先將其添加到哈希表中
- 然后插入到跳躍表的正確位置
跳躍表(Skip List)詳解
基本概念
跳躍表是一種概率平衡的數(shù)據(jù)結(jié)構(gòu),它通過維護(hù)多級索引來提高有序鏈表的查找效率。它結(jié)合了鏈表和類似二分查找的特性。
數(shù)據(jù)結(jié)構(gòu)實現(xiàn)
Redis 中跳躍表的核心定義(簡化版):
typedef struct zskiplistNode { robj *member; // 成員對象(如字符串) double score; // 分?jǐn)?shù)(用于排序) struct zskiplistNode *backward; // 后退指針(雙向鏈表) struct zskiplistLevel { struct zskiplistNode *forward; // 前進(jìn)指針 unsigned int span; // 跨度(用于計算排名) } level[]; // 柔性數(shù)組,表示節(jié)點的層級 } zskiplistNode; typedef struct zskiplist { struct zskiplistNode *header, *tail; unsigned long length; // 節(jié)點數(shù)量 int level; // 當(dāng)前最大層數(shù) } zskiplist;
關(guān)鍵特性
層級隨機(jī)生成:
- 新節(jié)點的層數(shù)由隨機(jī)算法決定(冪次定律)
- Redis 中最大層數(shù)為 32
int zslRandomLevel(void) { int level = 1; while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) level += 1; return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL; }
查找操作:
- 從最高層開始查找
- 如果當(dāng)前節(jié)點的值小于目標(biāo)值,則繼續(xù)前進(jìn)
- 否則下降一層繼續(xù)查找
- 時間復(fù)雜度:O(logN)
插入操作:
- 先查找插入位置
- 隨機(jī)生成新節(jié)點的層數(shù)
- 更新各層指針
- 時間復(fù)雜度:O(logN)
刪除操作:
- 類似插入的逆過程
- 時間復(fù)雜度:O(logN)
為什么選擇跳躍表?
Redis 選擇跳躍表而非平衡樹(如紅黑樹)的主要原因:
- 實現(xiàn)簡單:不需要復(fù)雜的旋轉(zhuǎn)操作
- 范圍查詢高效:底層鏈表天然有序,便于范圍操作
- 并發(fā)友好:更容易實現(xiàn)無鎖并發(fā)
- 平均性能好:雖然最壞情況不如平衡樹,但實際表現(xiàn)優(yōu)異
ZSET中與哈希表的協(xié)作
當(dāng)執(zhí)行 ZADD 命令時:
- 先在哈希表中查找/更新 member-score 映射
- 然后在跳躍表中插入/更新節(jié)點
- 保證兩個操作的原子性
這種雙數(shù)據(jù)結(jié)構(gòu)設(shè)計使得 ZSET 能夠:
- 快速判斷成員是否存在(哈希表)
- 高效執(zhí)行范圍查詢(跳躍表)
- 支持豐富的有序集合操作
Redis ZSET 實現(xiàn)滑動時間窗口限流
Redis ZSET除了實現(xiàn)排行榜之類的排序功能,還能根據(jù)擁有排序的特性,簡單的實現(xiàn)滑動時間窗口限流功能。
關(guān)鍵步驟
- 清理舊數(shù)據(jù):
ZREMRANGEBYSCORE key -inf (currentTime - windowSize)
- 統(tǒng)計當(dāng)前請求數(shù):
ZCARD key
- 檢查是否超限:比較當(dāng)前計數(shù)與閾值
- 記錄新請求:
ZADD key currentTime uniqueId
- 設(shè)置過期時間:
EXPIRE key windowSize + buffer
代碼實現(xiàn)(Spring Boot)
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Component; import java.util.Collections; import java.util.UUID; @Component public class SlidingWindowLimiter { private final RedisTemplate<String, String> redisTemplate; // Lua腳本(原子操作) private static final String LUA_SCRIPT = "local key = KEYS[1]\n" + "local now = tonumber(ARGV[1])\n" + "local window = tonumber(ARGV[2])\n" + "local maxRequests = tonumber(ARGV[3])\n" + "local requestId = ARGV[4]\n" + "\n" + "-- 1. 移除窗口外的舊數(shù)據(jù)\n" + "redis.call('ZREMRANGEBYSCORE', key, 0, now - window)\n" + "\n" + "-- 2. 獲取當(dāng)前請求數(shù)\n" + "local count = redis.call('ZCARD', key)\n" + "\n" + "-- 3. 檢查是否超限\n" + "if count >= maxRequests then\n" + " return 0\n" + "end\n" + "\n" + "-- 4. 記錄本次請求\n" + "redis.call('ZADD', key, now, requestId)\n" + "\n" + "-- 5. 刷新過期時間\n" + "redis.call('EXPIRE', key, window/1000 + 10)\n" + "return 1"; public SlidingWindowLimiter(RedisTemplate<String, String> redisTemplate) { this.redisTemplate = redisTemplate; } /** * 檢查請求是否允許通過 * @param key 限流鍵(如 user_123:api_pay) * @param windowMillis 窗口大?。ê撩耄? * @param maxRequests 窗口內(nèi)允許的最大請求數(shù) * @return true=允許, false=限流 */ public boolean allowRequest(String key, long windowMillis, int maxRequests) { // 構(gòu)造Redis Key String redisKey = "rate_limit:" + key; // 準(zhǔn)備腳本參數(shù) long currentTime = System.currentTimeMillis(); String requestId = UUID.randomUUID().toString(); // 執(zhí)行Lua腳本 DefaultRedisScript<Long> script = new DefaultRedisScript<>(LUA_SCRIPT, Long.class); Long result = redisTemplate.execute( script, Collections.singletonList(redisKey), currentTime, windowMillis, maxRequests, requestId ); return result != null && result == 1; } }
使用示例
@RestController public class PaymentController { @Autowired private SlidingWindowLimiter limiter; @PostMapping("/pay") public ResponseEntity<?> createPayment(@RequestBody PaymentRequest request) { // 構(gòu)造限流鍵: 用戶ID + 接口名 String key = "user_" + request.getUserId() + ":payment"; // 檢查限流: 每用戶每分鐘最多10次支付 if (!limiter.allowRequest(key, 60000, 10)) { return ResponseEntity.status(429).body("請求過于頻繁"); } // 執(zhí)行業(yè)務(wù)邏輯 paymentService.process(request); return ResponseEntity.ok("支付成功"); } }
到此這篇關(guān)于Redis中ZSet數(shù)據(jù)結(jié)構(gòu)與滑動窗口應(yīng)用的文章就介紹到這了,更多相關(guān)Redis ZSet滑動窗口內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
redis cluster支持pipeline的實現(xiàn)思路
本文給大家介紹redis cluster支持pipeline的實現(xiàn)思路,在 cluster 上執(zhí)行 pipeline 可能會由于 redis 節(jié)點擴(kuò)縮容 中途 redirection 切換連接導(dǎo)致結(jié)果丟失,具體細(xì)節(jié)問題請參考下本文2021-06-06redis列表類型_動力節(jié)點Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了redis列表類型的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-08-08Redis集群指定主從關(guān)系及動態(tài)增刪節(jié)點方式
這篇文章主要介紹了Redis集群指定主從關(guān)系及動態(tài)增刪節(jié)點方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-01-01redis和redisson實現(xiàn)分布式鎖的操作方法
使用 Redis 實現(xiàn)分布式鎖,最直接的想法是利用 setnx 和 expire 命令實現(xiàn)加鎖,這篇文章主要介紹了redis和redisson實現(xiàn)分布式鎖的操作方法,需要的朋友可以參考下2024-03-03redis 實現(xiàn)登陸次數(shù)限制的思路詳解
這篇文章主要介紹了redis 實現(xiàn)登陸次數(shù)限制的思路詳解,本文通過實例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2019-08-08如何監(jiān)聽Redis中Key值的變化(SpringBoot整合)
測試過程中我們有一部分常量值放入redis,共大部分應(yīng)用調(diào)用,但在測試過程中經(jīng)常有人會清空redis,回歸測試,下面這篇文章主要給大家介紹了關(guān)于如何監(jiān)聽Redis中Key值變化的相關(guān)資料,需要的朋友可以參考下2024-03-03