Redis限流实战:三种高效方案与选型指南

来源:站长平台作者:陈平安
导读:本期聚焦于小伙伴创作的《Redis限流实战:三种高效方案与选型指南》,敬请观看详情,探索知识的价值。以下视频、文章将为您系统阐述其核心内容与价值。如果您觉得《Redis限流实战:三种高效方案与选型指南》有用,将其分享出去将是对创作者最好的鼓励。

Redis实战记录之限制操作频率

在Web应用开发中,限制用户或接口的操作频率是常见的需求,比如防止接口被恶意刷取、控制用户发送验证码的频率等。Redis作为高性能的内存数据库,非常适合用来实现这类限流场景,本文将介绍几种基于Redis的限流实现方案。

常见限流场景与需求

我们先明确几个典型的限流需求,方便后续方案匹配:

  • 限制单个用户每分钟最多发送3次验证码

  • 限制单个IP每秒最多访问接口10次

  • 限制全局接口每分钟最多处理1000次请求

方案一:基于计数器的固定窗口限流

固定窗口限流是最简单的实现方式,核心思路是将时间划分为固定长度的窗口,统计窗口内的操作次数,超过阈值则拒绝请求。

实现逻辑

以“限制用户每分钟最多发送3次验证码”为例:

  1. 以用户ID和时间窗口的起始时间戳作为Redis的key,比如limit:sms:uid_123:1700000000,其中1700000000是当前分钟的时间戳

  2. 每次请求时,先对key执行自增操作,若自增后的值为1,则设置key的过期时间为窗口长度(60秒)

  3. 判断自增后的数值是否超过阈值3,超过则返回限流提示

代码示例(Java + Jedis)

import redis.clients.jedis.Jedis;

public class FixedWindowRateLimiter {
    private Jedis jedis;
    private int windowSeconds; // 窗口长度,单位秒
    private int maxCount; // 窗口内最大允许次数

    public FixedWindowRateLimiter(Jedis jedis, int windowSeconds, int maxCount) {
        this.jedis = jedis;
        this.windowSeconds = windowSeconds;
        this.maxCount = maxCount;
    }

    /**
     * 判断是否允许操作
     * @param key 限流标识,比如用户ID拼接业务标识
     * @return true表示允许,false表示被限流
     */
    public boolean tryAcquire(String key) {
        // 计算当前窗口的起始时间戳
        long currentTimestamp = System.currentTimeMillis() / 1000;
        long windowStart = currentTimestamp - (currentTimestamp % windowSeconds);
        String redisKey = key + ":" + windowStart;

        // 自增计数
        long count = jedis.incr(redisKey);
        // 第一次设置过期时间
        if (count == 1) {
            jedis.expire(redisKey, windowSeconds);
        }
        return count <= maxCount;
    }

    public static void main(String[] args) {
        Jedis jedis = new Jedis("https://www.ipipp.com", 6379);
        FixedWindowRateLimiter limiter = new FixedWindowRateLimiter(jedis, 60, 3);
        String userKey = "limit:sms:uid_123";
        for (int i = 0; i < 5; i++) {
            boolean allowed = limiter.tryAcquire(userKey);
            System.out.println("第" + (i + 1) + "次请求:" + (allowed ? "允许" : "被限流"));
        }
        jedis.close();
    }
}

方案优缺点

优点:实现简单,性能高,适合对限流精度要求不高的场景。

缺点:存在临界问题,比如在窗口切换的瞬间,可能出现两倍阈值的请求被放行。例如窗口1的59秒和窗口2的1秒各发送3次,实际2秒内就发送了6次,超过每分钟3次的限制。

方案二:基于滑动窗口的限流

滑动窗口限流可以解决固定窗口的临界问题,它统计的是最近一段时间内的操作次数,窗口随时间滑动。

实现逻辑

利用Redis的ZSET有序集合实现,核心思路是:

  1. 以操作的时间戳作为ZSET的分数(score),操作唯一标识作为成员(member)

  2. 每次请求时,先删除ZSET中超过窗口时间的旧记录

  3. 统计ZSET中当前的成员数量,若超过阈值则拒绝请求,否则添加当前请求记录到ZSET

代码示例(Java + Jedis)

import redis.clients.jedis.Jedis;
import java.util.UUID;

public class SlidingWindowRateLimiter {
    private Jedis jedis;
    private int windowSeconds; // 滑动窗口长度,单位秒
    private int maxCount; // 窗口内最大允许次数

    public SlidingWindowRateLimiter(Jedis jedis, int windowSeconds, int maxCount) {
        this.jedis = jedis;
        this.windowSeconds = windowSeconds;
        this.maxCount = maxCount;
    }

    /**
     * 判断是否允许操作
     * @param key 限流标识
     * @return true表示允许,false表示被限流
     */
    public boolean tryAcquire(String key) {
        long currentTimestamp = System.currentTimeMillis() / 1000;
        long windowStart = currentTimestamp - windowSeconds;

        // 删除窗口外的旧记录
        jedis.zremrangeByScore(key, 0, windowStart);

        // 统计当前窗口内的请求数
        long count = jedis.zcard(key);
        if (count >= maxCount) {
            return false;
        }

        // 添加当前请求记录,用UUID保证成员唯一
        jedis.zadd(key, currentTimestamp, UUID.randomUUID().toString());
        // 设置key的过期时间,避免无效数据长期占用内存
        jedis.expire(key, windowSeconds);
        return true;
    }

    public static void main(String[] args) {
        Jedis jedis = new Jedis("https://www.ipipp.com", 6379);
        SlidingWindowRateLimiter limiter = new SlidingWindowRateLimiter(jedis, 60, 3);
        String userKey = "limit:sms:sliding:uid_123";
        for (int i = 0; i < 5; i++) {
            boolean allowed = limiter.tryAcquire(userKey);
            System.out.println("第" + (i + 1) + "次请求:" + (allowed ? "允许" : "被限流"));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        jedis.close();
    }
}

方案优缺点

优点:解决了固定窗口的临界问题,限流精度更高。

缺点:实现相对复杂,ZSET的操作会占用更多内存,高并发场景下性能略低于固定窗口方案。

方案三:基于令牌桶的限流

令牌桶是更常用的限流算法,它有一个固定容量的桶,以恒定速率往桶里放入令牌,每次请求需要先获取令牌,获取到则允许操作,否则被限流。令牌桶允许一定程度的突发流量,因为桶内可以积累令牌。

实现逻辑

基于Redis的String结构实现令牌桶:

  1. key存储当前桶内的令牌数量,以及最后一次放令牌的时间

  2. 每次请求时,先计算从上次放令牌到现在应该新增的令牌数,更新桶内的令牌数量,同时更新最后放令牌时间

  3. 若当前令牌数大于0,则令牌数减1,允许请求;否则拒绝请求

代码示例(Java + Jedis)

import redis.clients.jedis.Jedis;
import java.util.List;

public class TokenBucketRateLimiter {
    private Jedis jedis;
    private int capacity; // 桶容量
    private int rate; // 每秒放令牌的速率
    private String keyPrefix = "limit:token:";

    public TokenBucketRateLimiter(Jedis jedis, int capacity, int rate) {
        this.jedis = jedis;
        this.capacity = capacity;
        this.rate = rate;
    }

    /**
     * 判断是否允许操作
     * @param key 限流标识
     * @return true表示允许,false表示被限流
     */
    public boolean tryAcquire(String key) {
        String redisKey = keyPrefix + key;
        long now = System.currentTimeMillis();

        // Lua脚本保证原子性操作
        String luaScript =
                "local key = KEYS[1]\n" +
                "local capacity = tonumber(ARGV[1])\n" +
                "local rate = tonumber(ARGV[2])\n" +
                "local now = tonumber(ARGV[3])\n" +
                "local last_data = redis.call('get', key)\n" +
                "local tokens, last_time\n" +
                "if not last_data then\n" +
                "    tokens = capacity\n" +
                "    last_time = now\n" +
                "else\n" +
                "    local parts = string.split(last_data, ':')\n" +
                "    tokens = tonumber(parts[1])\n" +
                "    last_time = tonumber(parts[2])\n" +
                "end\n" +
                "local delta = math.max(0, now - last_time)\n" +
                "local add_tokens = math.floor(delta * rate / 1000)\n" +
                "tokens = math.min(capacity, tokens + add_tokens)\n" +
                "last_time = now\n" +
                "if tokens >= 1 then\n" +
                "    tokens = tokens - 1\n" +
                "    redis.call('set', key, tokens .. ':' .. last_time)\n" +
                "    redis.call('expire', key, 60)\n" +
                "    return 1\n" +
                "else\n" +
                "    redis.call('set', key, tokens .. ':' .. last_time)\n" +
                "    redis.call('expire', key, 60)\n" +
                "    return 0\n" +
                "end";

        // 注意:Jedis的eval方法需要传入keys和args列表,这里简化调用,实际使用时需调整参数格式
        // 以下为示意代码,实际需根据Jedis版本调整eval调用方式
        Object result = jedis.eval(luaScript, 1, redisKey, String.valueOf(capacity), String.valueOf(rate), String.valueOf(now));
        return (Long) result == 1;
    }

    public static void main(String[] args) {
        Jedis jedis = new Jedis("https://www.ipipp.com", 6379);
        // 桶容量5,每秒放2个令牌
        TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(jedis, 5, 2);
        String userKey = "uid_123";
        for (int i = 0; i < 10; i++) {
            boolean allowed = limiter.tryAcquire(userKey);
            System.out.println("第" + (i + 1) + "次请求:" + (allowed ? "允许" : "被限流"));
        }
        jedis.close();
    }
}

方案优缺点

优点:支持突发流量,限流平滑,适合大多数接口限流场景。

缺点:实现相对复杂,需要借助Lua脚本保证原子性,避免并发问题。

方案选择建议

不同场景可以选择不同的限流方案:

  • 如果对限流精度要求不高,且实现简单优先,选择固定窗口限流

  • 如果需要避免临界问题,且流量不大,选择滑动窗口限流

  • 如果是接口限流,需要支持突发流量,优先选择令牌桶限流

注意事项

在实际使用Redis实现限流时,需要注意以下几点:

  • 高并发场景下,尽量使用Lua脚本保证操作的原子性,避免并发导致的计数错误

  • 合理设置Redis key的过期时间,避免无效数据占用过多内存

  • 限流阈值需要根据实际业务场景压测调整,避免阈值过高失去限流意义,或过低影响正常用户使用

  • 可以结合分布式场景,确保多实例部署时限流规则统一生效

Redis 限流方案 固定窗口 滑动窗口 令牌桶

免责声明:已尽一切努力确保本网站所含信息的准确性。网站部分内容来源于网络或由用户自行发表,内容观点不代表本站立场。本站是个人网站免费分享,内容仅供个人学习、研究或参考使用,如内容中引用了第三方作品,其版权归原作者所有。若内容触犯了您的权益,请联系我们进行处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。前端、网络、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握网站开发与运维所需的核心技术栈。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端逻辑,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。