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

高并发系统中的限流策略:滑动窗口限流与Redis实现

目录

引言

滑动窗口限流的基本概念

什么是滑动窗口限流?

滑动窗口限流的优点

固定窗口限流与滑动窗口限流的对比

固定窗口限流

滑动窗口限流的优势

基于Redis实现滑动窗口限流

Redis数据结构的选择

滑动窗口限流的实现步骤

基础实现代码

解决原子性问题

实战应用示例

扩展知识:其他限流策略

令牌桶算法

Redisson中的RRateLimiter

总结与实践建议

限流策略选择

实现注意事项

性能优化

参考资源


引言

        在当今互联网应用高速发展的时代,高并发系统已成为许多技术架构的标配。然而,伴随高并发而来的是系统稳定性挑战 —— 当请求量超过系统承载能力时,如何保证系统不崩溃?这就是限流技术发挥作用的地方。

        限流就像是为系统安装了一个"减压阀",能够在流量洪峰到来时,平滑地控制进入系统的请求数量,确保系统稳定运行。本文将聚焦于一种优秀的限流策略 —— **滑动窗口限流**,并结合Redis实现方案,为大家提供一套实用的高并发系统保护方案。

滑动窗口限流的基本概念

什么是滑动窗口限流?

        滑动窗口限流是一种动态流量控制策略,用于精确控制在特定时间段内允许处理的请求数量。它通过不断移动的时间窗口来统计和限制请求频率,达到保护系统的目的。

     从实现角度看,滑动窗口限流需要:

  1. 将时间划分为多个连续的小时间片段(如每秒)
  2. 定义一个较大的时间窗口(如10秒)
  3. 随着时间推移,窗口不断向前滑动
  4. 维护一个计数器,统计当前窗口内的请求总数
  5. 当新请求到达时,检查窗口内计数是否超过限制

滑动窗口限流的优点

滑动窗口限流相比其他限流策略具有以下优势:

  • 平滑控制流量:能够更自然地处理请求分布,避免突然的流量截断
  • 动态适应能力:随着时间推移,窗口动态调整,更符合实际流量变化规律
  • 精确的限流控制:可以精确限制任意时间窗口内的请求数量
  • 防止临界问题:解决了固定窗口在窗口边界可能出现的突发流量问题

固定窗口限流与滑动窗口限流的对比

        为了更好地理解滑动窗口限流的优势,我们先来了解一下更为简单的固定窗口限流。

固定窗口限流

        固定窗口限流将时间划分为固定长度的窗口(如1分钟),并在每个窗口内限制请求总数。当进入新窗口时,计数器会重置为零,重新开始计数。

固定窗口限流的主要问题:

        假设我们限制每分钟最多100个请求,如果在第一个窗口的最后10秒涌入95个请求,紧接着在第二个窗口的前10秒又涌入95个请求,那么在这20秒内系统实际需要处理190个请求 —— 远超我们期望的限制!这就是所谓的临界突发问题

滑动窗口限流的优势

滑动窗口通过动态调整时间窗口,有效解决了固定窗口的临界突发问题:

  • 在任意时间段内(如任意60秒),请求数都不会超过限制值
  • 随着旧请求逐渐滑出窗口,新的请求才被允许进入
  • 系统负载更加平均,避免突发流量对系统造成冲击

基于Redis实现滑动窗口限流

理论讲完了,接下来我们看看如何使用Redis实现一个高效的滑动窗口限流器。

Redis数据结构的选择

实现滑动窗口限流,我们需要一个能够同时记录时间戳和请求信息的数据结构。在Redis中,有序集合(ZSET) 是一个完美的选择:

  • 每个元素都有一个分数(score),我们可以用它存储请求的时间戳

  • 元素本身可以存储请求的唯一标识或详情

  • Redis提供了方便的区间操作,可以轻松移除窗口外的过期请求

我们的设计如下:

  • 键名格式limit_key_{资源名},例如:limit_key_login

  • 元素(member):请求的唯一标识(如UUID或请求详情的哈希值)

  • 分数(score):请求的时间戳

​​​​​​​

滑动窗口限流的实现步骤

  1. 定义滑动窗口大小,如60秒

  2. 每次收到新请求时,获取当前时间戳

  3. 计算窗口的起始时间点(当前时间 - 窗口大小)

  4. 删除窗口外的所有请求记录(使用ZREMRANGEBYSCORE命令)

  5. 统计窗口内的请求数量(使用ZCARD命令)

  6. 如果请求数量未超过限制,则添加新请求并允许通过;否则拒绝请求

基础实现代码

下面是一个简单的Java实现:

import redis.clients.jedis.Jedis;

public class SlidingWindowRateLimiter {
    private Jedis jedis;
    private int limit;
    
    public SlidingWindowRateLimiter(Jedis jedis, int limit) {
        this.jedis = jedis;
        this.limit = limit;
    }
    
    /**
     * 判断请求是否允许通过限流器
     * @param key 限流资源的标识
     * @return 是否允许请求通过
     */
    public boolean allowRequest(String key) {
        // 当前时间戳(毫秒)
        long currentTime = System.currentTimeMillis();
        
        // 计算窗口的起始时间(60秒前)
        long windowStart = currentTime - 60 * 1000;
        
        // 删除窗口外的所有数据
        jedis.zremrangeByScore(key, "-inf", String.valueOf(windowStart));
        
        // 统计窗口内的请求数量
        long currentRequests = jedis.zcard(key);
        
        // 如果请求数量未超过限制,则允许请求通过
        if (currentRequests < limit) {
            // 添加当前请求记录
            String requestId = String.valueOf(currentTime); // 实际应用中应使用UUID等唯一标识
            jedis.zadd(key, currentTime, requestId);
            return true;
        }
        
        // 请求数量已达到限制,拒绝请求
        return false;
    }
}

解决原子性问题

        上面的实现在高并发场景下存在原子性问题:检查窗口内请求数和添加新请求不是一个原子操作,可能导致超出限制的请求被错误地允许通过。

        为了解决这个问题,我们可以使用Redis的Lua脚本功能,确保整个限流逻辑在Redis服务器上以原子方式执行:

import redis.clients.jedis.Jedis;

public class SlidingWindowRateLimiter {
    private Jedis jedis;
    private int limit;
    private int windowSize; // 窗口大小(秒)
    
    public SlidingWindowRateLimiter(Jedis jedis, int limit, int windowSize) {
        this.jedis = jedis;
        this.limit = limit;
        this.windowSize = windowSize;
    }
    
    /**
     * 使用Lua脚本实现原子操作的限流逻辑
     * @param key 限流资源的标识
     * @return 是否允许请求通过
     */
    public boolean allowRequest(String key) {
        // 当前时间戳(毫秒)
        long currentTime = System.currentTimeMillis();
        
        // Lua脚本实现原子操作
        String luaScript = 
            "-- 计算窗口开始时间\n" +
            "local window_start = tonumber(ARGV[1]) - (tonumber(ARGV[3]) * 1000)\n" +
            "-- 删除窗口外的所有请求记录\n" +
            "redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', window_start)\n" +
            "-- 统计窗口内的请求数量\n" +
            "local current_requests = redis.call('ZCARD', KEYS[1])\n" +
            "-- 判断是否允许请求通过\n" +
            "if current_requests < tonumber(ARGV[2]) then\n" +
            "    -- 添加当前请求记录\n" +
            "    redis.call('ZADD', KEYS[1], ARGV[1], ARGV[1])\n" +
            "    return 1\n" +
            "else\n" +
            "    return 0\n" +
            "end";
        
        // 执行Lua脚本
        Object result = jedis.eval(
            luaScript, 
            1,                              // key的数量
            key,                            // key列表
            String.valueOf(currentTime),    // 当前时间戳
            String.valueOf(limit),          // 限制次数
            String.valueOf(windowSize)      // 窗口大小(秒)
        );
        
        // 解析结果(1=允许,0=拒绝)
        return (Long) result == 1;
    }
}

Lua脚本的优势

  • 确保整个限流逻辑在Redis服务器上原子执行,没有竞态条件
  • 减少客户端和服务器之间的网络往返,提高性能
  • 提供更高的安全性和可靠性

实战应用示例

import redis.clients.jedis.Jedis;

public class LoginRateLimiter {
    private static final String RATE_LIMITER_PREFIX = "limit_key_login_";
    private SlidingWindowRateLimiter limiter;
    
    public LoginRateLimiter(Jedis jedis) {
        // 创建限流器,设置限制为5次/分钟
        this.limiter = new SlidingWindowRateLimiter(jedis, 5, 60);
    }
    
    /**
     * 检查用户登录请求是否允许
     * @param userId 用户ID
     * @return 是否允许登录
     */
    public boolean allowLogin(String userId) {
        String key = RATE_LIMITER_PREFIX + userId;
        boolean allowed = limiter.allowRequest(key);
        
        if (!allowed) {
            System.out.println("用户" + userId + "登录频率过高,请稍后再试");
        }
        
        return allowed;
    }
    
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);
        LoginRateLimiter loginLimiter = new LoginRateLimiter(jedis);
        
        // 模拟用户登录
        String userId = "user123";
        for (int i = 0; i < 10; i++) {
            boolean allowed = loginLimiter.allowLogin(userId);
            System.out.println("登录尝试 #" + (i+1) + ": " + (allowed ? "成功" : "被限流"));
            
            try {
                Thread.sleep(500); // 每次尝试间隔500毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        jedis.close();
    }
}

扩展知识:其他限流策略

令牌桶算法

除了滑动窗口限流,令牌桶算法也是一种常用的限流策略。令牌桶算法的核心思想是:

  1. 系统以固定速率向桶中放入令牌
  2. 每个请求需要获取一个令牌才能执行
  3. 如果桶中没有令牌,请求将被阻塞或拒绝
  4. 桶可以存储一定数量的令牌,允许一定程度的突发流量

与滑动窗口的区别:

  • 令牌桶能够应对突发流量,而滑动窗口更适合平滑限流
  • 令牌桶关注的是平均处理速率,滑动窗口关注的是时间窗口内的请求总数
  • 令牌桶实现稍复杂,但提供更灵活的限流控制

Redisson中的RRateLimiter

        Redisson框架已经为我们提供了一个基于令牌桶算法的限流器——RRateLimiter,使用非常简便:

import org.redisson.api.RRateLimiter;
import org.redisson.api.RateIntervalUnit;
import org.redisson.api.RateType;
import org.redisson.api.RedissonClient;
import org.redisson.Redisson;
import org.redisson.config.Config;

public class RedissonRateLimiterExample {
    private static final String LIMIT_KEY_PREFIX = "api_rate_limit:";
    private RedissonClient redissonClient;
    
    public RedissonRateLimiterExample() {
        // 配置Redisson客户端
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379");
        this.redissonClient = Redisson.create(config);
    }
    
    /**
     * 尝试获取访问权限
     * @param key 资源标识
     * @param limit 限制次数
     * @param windowSize 时间窗口大小(秒)
     * @return 是否允许访问
     */
    public boolean tryAcquire(String key, int limit, int windowSize) {
        // 获取限流器
        RRateLimiter rateLimiter = redissonClient.getRateLimiter(LIMIT_KEY_PREFIX + key);
        
        // 初始化限流器(如果不存在)
        if (!rateLimiter.isExists()) {
            // 设置速率:每windowSize秒最多处理limit个请求
            rateLimiter.trySetRate(RateType.OVERALL, limit, windowSize, RateIntervalUnit.SECONDS);
        }
        
        // 尝试获取访问权限
        return rateLimiter.tryAcquire();
    }
    
    public void shutdown() {
        redissonClient.shutdown();
    }
    
    public static void main(String[] args) {
        RedissonRateLimiterExample example = new RedissonRateLimiterExample();
        
        // 模拟API调用(每10秒最多5次)
        String apiKey = "user_api";
        for (int i = 0; i < 10; i++) {
            boolean allowed = example.tryAcquire(apiKey, 5, 10);
            System.out.println("API调用 #" + (i+1) + ": " + (allowed ? "成功" : "被限流"));
            
            try {
                Thread.sleep(1000); // 每次调用间隔1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        example.shutdown();
    }
}

总结与实践建议

        通过本文,我们详细介绍了滑动窗口限流的原理和实现方式。以下是一些实践建议:

限流策略选择

  • 滑动窗口限流:适用于需要精确控制时间窗口内请求数量的场景,如登录接口、敏感操作等

  • 令牌桶算法:适用于允许短时间突发流量,但需要控制平均速率的场景,如普通API调用

实现注意事项

  • 在分布式环境中,一定要使用Redis等集中式存储确保限流的全局一致性

  • 使用Lua脚本保证限流操作的原子性,避免竞态条件

  • 为限流器设置合理的过期时间,避免长期占用Redis内存

  • 监控限流情况,及时调整限流参数以适应业务需求

性能优化

  • 可以使用Redis管道(Pipeline)或批量操作减少网络往返

  • 考虑使用本地缓存配合分布式限流,减轻Redis负担

  • 对于超高并发场景,可以采用多级限流策略,如本地限流+分布式限流

        最后,限流只是系统高可用的一个方面,完整的高并发系统保护还需要结合熔断、降级、负载均衡等多种技术手段,共同构建一个健壮的系统防护网。

参考资源

Redis官方文档:ZREMRANGEBYSCORE | Docs

Rediss on框架文档:8. Distributed locks and synchronizers · redisson/redisson Wiki · GitHub

限 流算法详解:Redisson分布式限流器RRateLimiter原理解析 · Issue #13 · oneone1995/blog · GitHub

相关文章:

  • 【QT】一文学会 QT 多线程(QThread )
  • Qt开发:QComboBox的使用
  • C++学习之云盘上传文件列表下载
  • C#基础学习(三)值类型和引用类型:编程世界的“现金“ vs “银行卡“,以及string这个“渣男“的叛变行为
  • 2059-Authentication plugin ‘caching_sha2_password‘ cannot be loaded
  • 【C++初阶】从零开始模拟实现vector(含迭代器失效详细讲解)
  • 大数据运维实战之YARN任务内存泄露排查实战:从节点掉线到精准定位的完整指南
  • 生成模型速通(Diffusion,VAE,GAN)
  • 理解使用Kubernetes对象
  • Java IO 流:从字节到字符再到Java 装饰者模式(Decorator Pattern),解析与应用掌握数据流动的艺术
  • macos设置docker可以ping通容器
  • Spring Boot(十五):集成Knife4j
  • 算法竞赛备赛——【数据结构】栈单调栈
  • 07_GRU模型
  • ChatGPT vs DeepSeek vs Copilot vs Claude:谁将问鼎AI王座?
  • HTML 表单处理进阶:验证与提交机制的学习心得与进度(一)
  • 优选算法的睿智之林:前缀和专题(一)
  • Codeforces Round 1012 (Div. 2)(ABCD)
  • 【Vue3入门2】02-记事本案例
  • redis命令
  • 全国人大常委会关于授权国务院在中国(新疆)自由贸易试验区暂时调整适用《中华人民共和国种子法》有关规定的决定
  • 鲁迅先生儿媳、周海婴先生夫人马新云女士逝世,享年94岁
  • 浙商银行一季度净赚超59亿微增0.61%,非息净收入降逾22%
  • 逛了6个小时的上海车展。有些不太成熟的感受。与你分享。
  • 人社部:将制定提前领取个人养老金相关办法
  • 习近平在上海考察时强调,加快建成具有全球影响力的科技创新高地