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等)都同时更新这两个数据结构来维护一致性。当添加一个新成员时:
- 会先将其添加到哈希表中
- 然后插入到跳跃表的正确位置
跳跃表(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;
关键特性
层级随机生成:
- 新节点的层数由随机算法决定(幂次定律)
- redis 中最大层数为 32
int zslrandomlevel(void) { int level = 1; while ((random()&0xffff) < (zskiplist_p * 0xffff)) level += 1; return (level<zskiplist_maxlevel) ? level : zskiplist_maxlevel; }
查找操作:
- 从最高层开始查找
- 如果当前节点的值小于目标值,则继续前进
- 否则下降一层继续查找
- 时间复杂度:o(logn)
插入操作:
- 先查找插入位置
- 随机生成新节点的层数
- 更新各层指针
- 时间复杂度:o(logn)
删除操作:
- 类似插入的逆过程
- 时间复杂度:o(logn)
为什么选择跳跃表?
redis 选择跳跃表而非平衡树(如红黑树)的主要原因:
- 实现简单:不需要复杂的旋转操作
- 范围查询高效:底层链表天然有序,便于范围操作
- 并发友好:更容易实现无锁并发
- 平均性能好:虽然最坏情况不如平衡树,但实际表现优异
zset中与哈希表的协作
当执行 zadd 命令时:
- 先在哈希表中查找/更新 member-score 映射
- 然后在跳跃表中插入/更新节点
- 保证两个操作的原子性
这种双数据结构设计使得 zset 能够:
- 快速判断成员是否存在(哈希表)
- 高效执行范围查询(跳跃表)
- 支持丰富的有序集合操作
redis zset 实现滑动时间窗口限流
redis zset除了实现排行榜之类的排序功能,还能根据拥有排序的特性,简单的实现滑动时间窗口限流功能。
关键步骤
- 清理旧数据:
zremrangebyscore key -inf (currenttime - windowsize)
- 统计当前请求数:
zcard key
- 检查是否超限:比较当前计数与阈值
- 记录新请求:
zadd key currenttime uniqueid
- 设置过期时间:
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 key string 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 { @autowired private 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("支付成功"); } }
到此这篇关于redis中zset数据结构与滑动窗口应用的文章就介绍到这了,更多相关redis zset滑动窗口内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论