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

如何在Redis中构建2小时时间桶键并实现Java滑动窗口限流?

Java + Redis 实现用户级限流(2小时窗口/200次阈值)

核心逻辑说明

针对每个用户维护一个2小时时间窗口,窗口内请求累计达200次则触发限流。窗口规则为:从首次请求时间起算2小时,超时后重置计数器;若窗口内请求数未达阈值,每次请求递增计数。

由于并发场景下存在竞态风险(如多个请求同时判断窗口状态导致计数不准),所有Redis操作需通过Lua脚本原子执行,保证逻辑一致性。

Redis数据结构选择

使用Redis哈希表(Hash)存储每个用户的限流信息,键格式为rate_limit:user:{USERID},哈希表包含两个字段:

  • count:当前窗口内的请求计数
  • expire_ts:窗口过期时间戳(毫秒级)

具体实现

1. 原子性Lua脚本

Lua脚本负责完成以下原子操作:

  1. 检查用户对应的哈希键是否存在
  2. 若不存在:初始化计数为1,设置窗口过期时间为当前时间+2小时,返回1(放行)
  3. 若存在:
    • 获取当前窗口的过期时间和计数
    • 判断当前时间是否超过窗口过期时间:
      • 是:重置计数为1,更新过期时间,返回1(放行)
      • 否:递增计数,若计数≥200返回0(限流),否则返回当前计数(放行)
-- 限流Lua脚本
local key = KEYS[1]
local threshold = tonumber(ARGV[1])
local window_ms = tonumber(ARGV[2])
local current_ts = tonumber(ARGV[3])

-- 获取哈希表中的计数和过期时间
local count = tonumber(redis.call('HGET', key, 'count')) or 0
local expire_ts = tonumber(redis.call('HGET', key, 'expire_ts')) or 0

if current_ts > expire_ts then
    -- 窗口已过期,重置
    redis.call('HSET', key, 'count', 1)
    redis.call('HSET', key, 'expire_ts', current_ts + window_ms)
    -- 设置键的过期时间,避免内存泄漏(窗口过期后额外保留1分钟)
    redis.call('EXPIRE', key, window_ms / 1000 + 60)
    return 1
else
    if count >= threshold then
        -- 达到阈值,触发限流
        return 0
    else
        -- 递增计数
        local new_count = count + 1
        redis.call('HSET', key, 'count', new_count)
        return new_count
    end
end

2. Java代码集成(Spring Data Redis示例)

依赖配置

确保项目引入Spring Data Redis依赖(Maven示例):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

限流工具类实现

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.concurrent.TimeUnit;

@Component
public class UserRateLimiter {
    // 限流阈值:200次
    private static final int THRESHOLD = 200;
    // 窗口时长:2小时(转毫秒)
    private static final long WINDOW_MS = TimeUnit.HOURS.toMillis(2);
    // Redis键前缀
    private static final String KEY_PREFIX = "rate_limit:user:";

    private final RedisTemplate<String, Object> redisTemplate;
    private final DefaultRedisScript<Long> rateLimitScript;

    public UserRateLimiter(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
        // 初始化Lua脚本
        rateLimitScript = new DefaultRedisScript<>();
        rateLimitScript.setScriptText(getLuaScriptContent());
        rateLimitScript.setResultType(Long.class);
    }

    /**
     * 判断用户请求是否被限流
     * @param userId 用户ID
     * @return true=被限流,false=放行
     */
    public boolean isLimited(String userId) {
        String key = KEY_PREFIX + userId;
        long currentTs = System.currentTimeMillis();
        // 执行Lua脚本
        Long result = redisTemplate.execute(
                rateLimitScript,
                Collections.singletonList(key),
                THRESHOLD,
                WINDOW_MS,
                currentTs
        );
        // 结果为0表示触发限流,其他值为当前计数(放行)
        return result != null && result == 0;
    }

    /**
     * 读取Lua脚本内容
     */
    private String getLuaScriptContent() {
        return "-- 限流Lua脚本\n" +
                "local key = KEYS[1]\n" +
                "local threshold = tonumber(ARGV[1])\n" +
                "local window_ms = tonumber(ARGV[2])\n" +
                "local current_ts = tonumber(ARGV[3])\n" +
                "\n" +
                "local count = tonumber(redis.call('HGET', key, 'count')) or 0\n" +
                "local expire_ts = tonumber(redis.call('HGET', key, 'expire_ts')) or 0\n" +
                "\n" +
                "if current_ts > expire_ts then\n" +
                "    redis.call('HSET', key, 'count', 1)\n" +
                "    redis.call('HSET', key, 'expire_ts', current_ts + window_ms)\n" +
                "    redis.call('EXPIRE', key, window_ms / 1000 + 60)\n" +
                "    return 1\n" +
                "else\n" +
                "    if count >= threshold then\n" +
                "        return 0\n" +
                "    else\n" +
                "        local new_count = count + 1\n" +
                "        redis.call('HSET', key, 'count', new_count)\n" +
                "        return new_count\n" +
                "    end\n" +
                "end";
    }
}

3. 业务代码中使用

@RestController
public class DemoController {
    private final UserRateLimiter userRateLimiter;

    public DemoController(UserRateLimiter userRateLimiter) {
        this.userRateLimiter = userRateLimiter;
    }

    @GetMapping("/api/resource")
    public String accessResource(@RequestParam String userId) {
        if (userRateLimiter.isLimited(userId)) {
            return "请求过于频繁,请2小时后再试";
        }
        // 处理业务逻辑
        return "请求成功";
    }
}

补充:严格滑动窗口实现(可选)

如果需要计算当前时间往前2小时内的所有请求数(而非固定窗口),可使用Redis有序集合(ZSet)实现:

  1. 每个请求到来时,向ZSet添加元素(value为唯一标识,score为当前时间戳)
  2. 删除ZSet中score小于(当前时间-2小时)的元素
  3. 统计ZSet元素数量,若≥200则触发限流

实现逻辑类似,核心是用ZSet维护请求时间戳并定时清理过期元素。


内容的提问来源于stack exchange,提问作者Yathish Manjunath

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 00:52:38