it编程 > 数据库 > Redis

Redis中ZSet数据结构与滑动窗口应用实现

43人参与 2025-07-30 Redis

redis 的 zset(有序集合) 是一种结合了 哈希表跳跃表(skip list) 的混合数据结构,既能实现 o(1) 复杂度的成员存在性判断,又能以 o(logn) 复杂度维护有序性。

redis zset 数据存储机制

zset 有两种实现机制:

skiplist + hashtable

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

跳表(skiplist)

哈希表(hashtable)

ziplist

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

数据一致性

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;

关键特性

层级随机生成

int zslrandomlevel(void) {
    int level = 1;
    while ((random()&0xffff) < (zskiplist_p * 0xffff))
        level += 1;
    return (level<zskiplist_maxlevel) ? level : zskiplist_maxlevel;
}

查找操作

插入操作

删除操作

为什么选择跳跃表?

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

zset中与哈希表的协作

当执行 zadd 命令时:

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

redis zset 实现滑动时间窗口限流

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

关键步骤

代码实现(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滑动窗口内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

您想发表意见!!点此发布评论

推荐阅读

使用Redis实现API网关或单个服务的请求限流的具体代码

07-30

Redis客户端连接机制的实现方案

07-31

Redis设置永久密码方式

07-31

Redis修改密码的实现方式

07-31

Ubuntu查看文件最后更新时间的全面指南

07-31

Redis的数据类型解读

07-29

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论