当前位置: 首页 > news >正文

Redis - ZSet数据结构与滑动窗口应用

Redis 的 ZSET(有序集合) 是一种结合了 哈希表跳跃表(Skip List) 的混合数据结构,既能实现 O(1) 复杂度的成员存在性判断,又能以 O(logN) 复杂度维护有序性。

Redis ZSET 数据存储机制

ZSET 有两种实现机制:

SkipList + HashTable

数据实际上是同时存在于两个数据结构中的

  • 跳表(SkipList)

    • score 排序存储 member

    • 支持范围查询(ZRANGE 等命令)

    • 维护成员的有序性

  • 哈希表(HashTable)

    • 存储 member -> score 的映射

    • 用于快速判断成员是否存在(O(1) 复杂度)

    • 直接获取成员的分数(ZSCORE 命令)

ZipList

  • ZipList:对于小型有序集合(元素少且 member 小),Redis 会使用 ziplist 编码来节省内存,只有当元素数量或大小超过阈值时才会转换为真正的跳跃表+哈希表实现。

    • (元素, score) 对顺序存储

数据一致性

Redis 通过保证所有写操作(ZADD/ZREM等)都同时更新这两个数据结构来维护一致性。当添加一个新成员时:

  1. 会先将其添加到哈希表中

  2. 然后插入到跳跃表的正确位置

跳跃表(Skip List)详解

基本概念

跳跃表是一种概率平衡的数据结构,它通过维护多级索引来提高有序链表的查找效率。它结合了链表和类似二分查找的特性。

数据结构实现

Redis 中跳跃表的核心定义(简化版):

typedef struct zskiplistNode {robj *member;          // 成员对象(如字符串)double score;          // 分数(用于排序)struct zskiplistNode *backward; // 后退指针(双向链表)struct zskiplistLevel {struct zskiplistNode *forward; // 前进指针unsigned int span;  // 跨度(用于计算排名)} level[];             // 柔性数组,表示节点的层级
} zskiplistNode;typedef struct zskiplist {struct zskiplistNode *header, *tail;unsigned long length;  // 节点数量int level;            // 当前最大层数
} zskiplist;

关键特性

  1. 层级随机生成

    • 新节点的层数由随机算法决定(幂次定律)

    • Redis 中最大层数为 32

    int zslRandomLevel(void) {int level = 1;while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))level += 1;return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
    }
  2. 查找操作

    • 从最高层开始查找

    • 如果当前节点的值小于目标值,则继续前进

    • 否则下降一层继续查找

    • 时间复杂度:O(logN)

  3. 插入操作

    • 先查找插入位置

    • 随机生成新节点的层数

    • 更新各层指针

    • 时间复杂度:O(logN)

  4. 删除操作

    • 类似插入的逆过程

    • 时间复杂度:O(logN)

为什么选择跳跃表?

Redis 选择跳跃表而非平衡树(如红黑树)的主要原因:

  1. 实现简单:不需要复杂的旋转操作

  2. 范围查询高效:底层链表天然有序,便于范围操作

  3. 并发友好:更容易实现无锁并发

  4. 平均性能好:虽然最坏情况不如平衡树,但实际表现优异

ZSET中与哈希表的协作

当执行 ZADD 命令时:

  1. 先在哈希表中查找/更新 member-score 映射

  2. 然后在跳跃表中插入/更新节点

  3. 保证两个操作的原子性

这种双数据结构设计使得 ZSET 能够:

  • 快速判断成员是否存在(哈希表)

  • 高效执行范围查询(跳跃表)

  • 支持丰富的有序集合操作

Redis ZSET 实现滑动时间窗口限流

Redis ZSET除了实现排行榜之类的排序功能,还能根据拥有排序的特性,简单的实现滑动时间窗口限流功能。

关键步骤

  1. 清理旧数据ZREMRANGEBYSCORE key -inf (currentTime - windowSize)

  2. 统计当前请求数ZCARD key

  3. 检查是否超限:比较当前计数与阈值

  4. 记录新请求ZADD key currentTime uniqueId

  5. 设置过期时间EXPIRE key windowSize + buffer

代码实现(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. 移除窗口外的旧数据\n" +"redis.call('ZREMRANGEBYSCORE', key, 0, now - window)\n" +"\n" +"-- 2. 获取当前请求数\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 窗口内允许的最大请求数* @return true=允许, false=限流*/public boolean allowRequest(String key, long windowMillis, int maxRequests) {// 构造Redis KeyString redisKey = "rate_limit:" + key;// 准备脚本参数long currentTime = System.currentTimeMillis();String requestId = UUID.randomUUID().toString();// 执行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 {@Autowiredprivate SlidingWindowLimiter limiter;@PostMapping("/pay")public ResponseEntity<?> createPayment(@RequestBody PaymentRequest request) {// 构造限流键: 用户ID + 接口名String key = "user_" + request.getUserId() + ":payment";// 检查限流: 每用户每分钟最多10次支付if (!limiter.allowRequest(key, 60000, 10)) {return ResponseEntity.status(429).body("请求过于频繁");}// 执行业务逻辑paymentService.process(request);return ResponseEntity.ok("支付成功");}
}

http://www.dtcms.com/a/293200.html

相关文章:

  • 在 WebSocket 中使用 @Autowired 时遇到空指针异常
  • 二、Vue常用指令:v-bind、v-model、v-on
  • 【Python】常见模块及其用法
  • MSTP技术
  • 建造者模式:构建复杂对象的优雅方式
  • c练习-c基础
  • Pulsar存储计算分离架构设计之Broker无状态
  • vscode目录,右键菜单加入用VSCode打开文件和文件夹(快速解决)(含删除)(脚本)
  • gma视角的历史地图集3:自夏至宋3000年5座洛阳城
  • 【锁】MySQL中有哪几种锁?
  • opencv-图像处理
  • 视频编解码技术的未来趋势:从H.266到AI驱动的绿色革命
  • 【AI 学习笔记】Tool Calling:让 AI 不再“纸上谈兵“
  • 深入解析 Fetch API 的 credentials 属性:Cookie 携带机制
  • 洛谷 P3478 [POI 2008] STA-Station
  • Ollama Docker 容器向容器内传输AI模型并挂载模型
  • 基于快速S变换的配电网故障选线
  • Android开发:Java与Kotlin深度对比
  • IDC权威认可:瑞数信息双项入选《中国大模型安全保护市场概览》
  • CSS+JavaScript 禁用浏览器复制功能的几种方法
  • AWE2026启动:加码AI科技,双展区联动开启产业新格局
  • LeetCode 刷题【11. 盛最多水的容器】
  • Zap日志库指南
  • PCIe Base Specification解析(三)
  • java多线程编程自用笔记
  • 论文笔记:EMR-MERGING: Tuning-Free High-Performance Model Merging
  • 2025.7.22 测试 总结
  • Qt/C++源码/监控设备模拟器/支持onvif和gb28181/多路批量模拟/虚拟监控摄像头
  • 50天50个小项目 (Vue3 + Tailwindcss V4) ✨ | ImageCarousel(图片轮播组件)
  • linux应用:spi_ioc_transfer结构cs_change说明