You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

寻求基于Redis+滚动窗口限流+退避策略的Java限流实现方案

基于Redis的Java滚动窗口令牌桶限流实现(支持退避时长)

核心思路

要实现和redis-token-bucket-ratelimiter一致的基于时间戳的滚动窗口限流,且返回退避时长,必须依赖Redis Lua脚本保证原子性(避免并发场景下的计数误差)。核心逻辑是:

  • 用Redis有序集合(ZSet)存储滚动窗口内的令牌消耗记录,以时间戳为score、消耗令牌数为value
  • 每次请求先清理窗口外的过期记录,计算窗口内已消耗的总令牌数
  • 根据剩余令牌判断是否允许请求,若拒绝则计算需要等待的退避时长

实现代码

1. 定义返回结果类

public class RollingLimitResult {
    private int limit;
    private int remaining;
    private boolean rejected;
    private long retryDelta;
    private boolean forced;

    public RollingLimitResult(int limit, int remaining, boolean rejected, long retryDelta, boolean forced) {
        this.limit = limit;
        this.remaining = remaining;
        this.rejected = rejected;
        this.retryDelta = retryDelta;
        this.forced = forced;
    }

    // 按需生成getter方法
    public int getLimit() { return limit; }
    public int getRemaining() { return remaining; }
    public boolean isRejected() { return rejected; }
    public long getRetryDelta() { return retryDelta; }
    public boolean isForced() { return forced; }
}

2. Redis Lua脚本(核心原子逻辑)

-- KEYS[1] = 限流完整key
-- ARGV[1] = 窗口间隔(毫秒)
-- ARGV[2] = 窗口内令牌限额
-- ARGV[3] = 请求令牌数
-- ARGV[4] = 是否强制放行(1/0)
-- ARGV[5] = 当前时间戳(毫秒)

local key = KEYS[1]
local interval = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local requestAmount = tonumber(ARGV[3])
local force = tonumber(ARGV[4]) == 1
local now = tonumber(ARGV[5])

-- 清理窗口外的过期记录
local windowStart = now - interval
redis.call('ZREMRANGEBYSCORE', key, '-inf', windowStart)

-- 计算窗口内已消耗的总令牌数
local usedTokens = tonumber(redis.call('ZSUM', key) or 0)
local remainingTokens = limit - usedTokens

-- 判断是否允许请求
local allowed = remainingTokens >= requestAmount or force
local newUsedTokens = usedTokens + requestAmount

if allowed then
    -- 记录本次令牌消耗
    redis.call('ZADD', key, now, requestAmount)
    -- 设置key过期时间,避免内存泄漏
    redis.call('EXPIRE', key, math.ceil(interval / 1000) + 1)
end

-- 计算退避时长(仅拒绝时有效)
local retryDelta = 0
if not allowed then
    -- 计算需要补充的令牌数
    local needed = requestAmount - remainingTokens
    -- 按令牌恢复速率计算退避时长:恢复速率 = 总限额/窗口间隔(令牌/毫秒)
    retryDelta = math.ceil(needed * interval / limit)
end

-- 返回结果:[限额, 剩余令牌, 是否拒绝(1=是/0=否), 退避时长, 是否强制(1=是/0=否)]
return {
    limit,
    remainingTokens - (allowed and requestAmount or 0),
    allowed and 0 or 1,
    retryDelta,
    force and 1 or 0
}

3. Java限流工具类封装

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.List;

public class RollingLimit {
    private final RedisTemplate<String, Object> redisTemplate;
    private final DefaultRedisScript<List<Long>> limitScript;
    private final int interval;
    private final int limit;
    private final String prefix;

    public RollingLimit(RedisTemplate<String, Object> redisTemplate, int interval, int limit, String prefix) {
        this.redisTemplate = redisTemplate;
        this.interval = interval;
        this.limit = limit;
        this.prefix = prefix;

        // 初始化Lua脚本(建议将脚本放在resources目录下读取,避免硬编码)
        this.limitScript = new DefaultRedisScript<>();
        limitScript.setScriptText("上面的Lua脚本内容");
        limitScript.setResultType(List.class);
    }

    public RollingLimitResult use(String reqKey, int reqAmount) {
        return use(reqKey, reqAmount, false);
    }

    public RollingLimitResult use(String reqKey, int reqAmount, boolean force) {
        String fullKey = prefix + ":" + reqKey;
        long now = System.currentTimeMillis();

        List<String> keys = Collections.singletonList(fullKey);
        List<Object> args = List.of(interval, limit, reqAmount, force ? 1 : 0, now);

        // 执行Lua脚本
        List<Long> result = redisTemplate.execute(limitScript, keys, args.toArray());

        // 解析返回结果
        int resultLimit = result.get(0).intValue();
        int remaining = result.get(1).intValue();
        boolean rejected = result.get(2) == 1;
        long retryDelta = result.get(3);
        boolean forced = result.get(4) == 1;

        return new RollingLimitResult(resultLimit, remaining, rejected, retryDelta, forced);
    }
}

关键逻辑说明

  1. 滚动窗口实现:通过ZREMRANGEBYSCORE清理窗口起始时间之前的所有记录,确保每次计算的都是当前窗口内的令牌消耗。
  2. 退避时长计算:当令牌不足时,根据令牌恢复速率(总限额/窗口间隔)计算等待时间,保证等待后窗口内有足够令牌处理请求。
  3. 原子性保障:所有计算和更新操作在Lua脚本中原子执行,避免并发请求导致的计数错误。
  4. 内存优化:给ZSet设置过期时间,避免无用key占用Redis内存。

使用示例

// 假设已初始化RedisTemplate
RollingLimit limiter = new RollingLimit(redisTemplate, 60000, 100, "api:rate:limit");

try {
    RollingLimitResult result = limiter.use("user:123", 1);
    if (result.isRejected()) {
        System.out.println("请求被限流,需等待" + result.getRetryDelta() + "毫秒");
    } else {
        System.out.println("请求允许,剩余令牌:" + result.getRemaining());
    }
} catch (Exception e) {
    // 处理Redis调用异常
    e.printStackTrace();
}

内容的提问来源于stack exchange,提问作者Rajagopal M

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 19:31:09