43人参与 • 2025-07-30 • Redis
redis 的 zset(有序集合) 是一种结合了 哈希表 和 跳跃表(skip list) 的混合数据结构,既能实现 o(1) 复杂度的成员存在性判断,又能以 o(logn) 复杂度维护有序性。
zset 有两种实现机制:
数据实际上是同时存在于两个数据结构中的
跳表(skiplist)
score
排序存储 member
哈希表(hashtable)
member -> score
的映射ziplist:对于小型有序集合(元素少且 member 小),redis 会使用 ziplist 编码来节省内存,只有当元素数量或大小超过阈值时才会转换为真正的跳跃表+哈希表实现。
(元素, score)
对顺序存储redis 通过保证所有写操作(zadd/zrem等)都同时更新这两个数据结构来维护一致性。当添加一个新成员时:
跳跃表是一种概率平衡的数据结构,它通过维护多级索引来提高有序链表的查找效率。它结合了链表和类似二分查找的特性。
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;
层级随机生成:
int zslrandomlevel(void) { int level = 1; while ((random()&0xffff) < (zskiplist_p * 0xffff)) level += 1; return (level<zskiplist_maxlevel) ? level : zskiplist_maxlevel; }
查找操作:
插入操作:
删除操作:
redis 选择跳跃表而非平衡树(如红黑树)的主要原因:
当执行 zadd 命令时:
这种双数据结构设计使得 zset 能够:
redis zset除了实现排行榜之类的排序功能,还能根据拥有排序的特性,简单的实现滑动时间窗口限流功能。
zremrangebyscore key -inf (currenttime - windowsize)
zcard key
zadd key currenttime uniqueid
expire key windowsize + buffer
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滑动窗口内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论