Redis中ZSet數(shù)據(jù)結(jié)構(gòu)與滑動(dòng)窗口應(yīng)用實(shí)現(xiàn)
Redis 的 ZSET(有序集合) 是一種結(jié)合了 哈希表 和 跳躍表(Skip List) 的混合數(shù)據(jù)結(jié)構(gòu),既能實(shí)現(xiàn) O(1) 復(fù)雜度的成員存在性判斷,又能以 O(logN) 復(fù)雜度維護(hù)有序性。
Redis ZSET 數(shù)據(jù)存儲(chǔ)機(jī)制
ZSET 有兩種實(shí)現(xiàn)機(jī)制:
SkipList + HashTable
數(shù)據(jù)實(shí)際上是同時(shí)存在于兩個(gè)數(shù)據(jù)結(jié)構(gòu)中的
跳表(SkipList)
- 按
score排序存儲(chǔ)member - 支持范圍查詢(ZRANGE 等命令)
- 維護(hù)成員的有序性
哈希表(HashTable)
- 存儲(chǔ)
member -> score的映射 - 用于快速判斷成員是否存在(O(1) 復(fù)雜度)
- 直接獲取成員的分?jǐn)?shù)(ZSCORE 命令)
ZipList
ZipList:對(duì)于小型有序集合(元素少且 member ?。琑edis 會(huì)使用 ziplist 編碼來(lái)節(jié)省內(nèi)存,只有當(dāng)元素?cái)?shù)量或大小超過(guò)閾值時(shí)才會(huì)轉(zhuǎn)換為真正的跳躍表+哈希表實(shí)現(xiàn)。
- 按
(元素, score)對(duì)順序存儲(chǔ)
數(shù)據(jù)一致性
Redis 通過(guò)保證所有寫操作(ZADD/ZREM等)都同時(shí)更新這兩個(gè)數(shù)據(jù)結(jié)構(gòu)來(lái)維護(hù)一致性。當(dāng)添加一個(gè)新成員時(shí):
- 會(huì)先將其添加到哈希表中
- 然后插入到跳躍表的正確位置
跳躍表(Skip List)詳解
基本概念
跳躍表是一種概率平衡的數(shù)據(jù)結(jié)構(gòu),它通過(guò)維護(hù)多級(jí)索引來(lái)提高有序鏈表的查找效率。它結(jié)合了鏈表和類似二分查找的特性。

數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)
Redis 中跳躍表的核心定義(簡(jiǎn)化版):
typedef struct zskiplistNode {
robj *member; // 成員對(duì)象(如字符串)
double score; // 分?jǐn)?shù)(用于排序)
struct zskiplistNode *backward; // 后退指針(雙向鏈表)
struct zskiplistLevel {
struct zskiplistNode *forward; // 前進(jìn)指針
unsigned int span; // 跨度(用于計(jì)算排名)
} level[]; // 柔性數(shù)組,表示節(jié)點(diǎn)的層級(jí)
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length; // 節(jié)點(diǎn)數(shù)量
int level; // 當(dāng)前最大層數(shù)
} zskiplist;關(guān)鍵特性
層級(jí)隨機(jī)生成:
- 新節(jié)點(diǎn)的層數(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é)點(diǎn)的值小于目標(biāo)值,則繼續(xù)前進(jìn)
- 否則下降一層繼續(xù)查找
- 時(shí)間復(fù)雜度:O(logN)
插入操作:
- 先查找插入位置
- 隨機(jī)生成新節(jié)點(diǎn)的層數(shù)
- 更新各層指針
- 時(shí)間復(fù)雜度:O(logN)
刪除操作:
- 類似插入的逆過(guò)程
- 時(shí)間復(fù)雜度:O(logN)
為什么選擇跳躍表?
Redis 選擇跳躍表而非平衡樹(如紅黑樹)的主要原因:
- 實(shí)現(xiàn)簡(jiǎn)單:不需要復(fù)雜的旋轉(zhuǎn)操作
- 范圍查詢高效:底層鏈表天然有序,便于范圍操作
- 并發(fā)友好:更容易實(shí)現(xiàn)無(wú)鎖并發(fā)
- 平均性能好:雖然最壞情況不如平衡樹,但實(shí)際表現(xiàn)優(yōu)異
ZSET中與哈希表的協(xié)作
當(dāng)執(zhí)行 ZADD 命令時(shí):
- 先在哈希表中查找/更新 member-score 映射
- 然后在跳躍表中插入/更新節(jié)點(diǎn)
- 保證兩個(gè)操作的原子性
這種雙數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)使得 ZSET 能夠:
- 快速判斷成員是否存在(哈希表)
- 高效執(zhí)行范圍查詢(跳躍表)
- 支持豐富的有序集合操作
Redis ZSET 實(shí)現(xiàn)滑動(dòng)時(shí)間窗口限流
Redis ZSET除了實(shí)現(xiàn)排行榜之類的排序功能,還能根據(jù)擁有排序的特性,簡(jiǎn)單的實(shí)現(xiàn)滑動(dòng)時(shí)間窗口限流功能。

關(guān)鍵步驟
- 清理舊數(shù)據(jù):
ZREMRANGEBYSCORE key -inf (currentTime - windowSize) - 統(tǒng)計(jì)當(dāng)前請(qǐng)求數(shù):
ZCARD key - 檢查是否超限:比較當(dāng)前計(jì)數(shù)與閾值
- 記錄新請(qǐng)求:
ZADD key currentTime uniqueId - 設(shè)置過(guò)期時(shí)間:
EXPIRE key windowSize + buffer
代碼實(shí)現(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)前請(qǐ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. 記錄本次請(qǐng)求\n" +
"redis.call('ZADD', key, now, requestId)\n" +
"\n" +
"-- 5. 刷新過(guò)期時(shí)間\n" +
"redis.call('EXPIRE', key, window/1000 + 10)\n" +
"return 1";
public SlidingWindowLimiter(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 檢查請(qǐng)求是否允許通過(guò)
* @param key 限流鍵(如 user_123:api_pay)
* @param windowMillis 窗口大小(毫秒)
* @param maxRequests 窗口內(nèi)允許的最大請(qǐng)求數(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("請(qǐng)求過(guò)于頻繁");
}
// 執(zhí)行業(yè)務(wù)邏輯
paymentService.process(request);
return ResponseEntity.ok("支付成功");
}
}到此這篇關(guān)于Redis中ZSet數(shù)據(jù)結(jié)構(gòu)與滑動(dòng)窗口應(yīng)用的文章就介紹到這了,更多相關(guān)Redis ZSet滑動(dòng)窗口內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
redis cluster支持pipeline的實(shí)現(xiàn)思路
本文給大家介紹redis cluster支持pipeline的實(shí)現(xiàn)思路,在 cluster 上執(zhí)行 pipeline 可能會(huì)由于 redis 節(jié)點(diǎn)擴(kuò)縮容 中途 redirection 切換連接導(dǎo)致結(jié)果丟失,具體細(xì)節(jié)問(wèn)題請(qǐng)參考下本文2021-06-06
redis列表類型_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了redis列表類型的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08
Redis集群指定主從關(guān)系及動(dòng)態(tài)增刪節(jié)點(diǎn)方式
這篇文章主要介紹了Redis集群指定主從關(guān)系及動(dòng)態(tài)增刪節(jié)點(diǎn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01
redis和redisson實(shí)現(xiàn)分布式鎖的操作方法
使用 Redis 實(shí)現(xiàn)分布式鎖,最直接的想法是利用 setnx 和 expire 命令實(shí)現(xiàn)加鎖,這篇文章主要介紹了redis和redisson實(shí)現(xiàn)分布式鎖的操作方法,需要的朋友可以參考下2024-03-03
redis 實(shí)現(xiàn)登陸次數(shù)限制的思路詳解
這篇文章主要介紹了redis 實(shí)現(xiàn)登陸次數(shù)限制的思路詳解,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-08-08
如何監(jiān)聽Redis中Key值的變化(SpringBoot整合)
測(cè)試過(guò)程中我們有一部分常量值放入redis,共大部分應(yīng)用調(diào)用,但在測(cè)試過(guò)程中經(jīng)常有人會(huì)清空redis,回歸測(cè)試,下面這篇文章主要給大家介紹了關(guān)于如何監(jiān)聽Redis中Key值變化的相關(guān)資料,需要的朋友可以參考下2024-03-03

