分布式限流 = Redis 全局计数 + Lua 脚本保证原子性

  • Redis:存全局计数器,所有服务共享
  • Lua脚本:保证原子性,判断 + 计数 + 过期一步原子操作完成,避免并发计数错误
  • 固定窗口算法(最简单、企业最常用)

注:Redis实现分布式限流是强一致性,因为:

  • Redis 是单线程执行命令,同一时间只有一个请求在执行计数、判断逻辑。

  • 限流逻辑全部封装在 Lua 脚本里, 判断 + 计数 + 过期 是一个原子操作,不可分割。

  • 集群所有机器都访问同一个 Redis,全局唯一计数,没有本地计数,不会出现误差

一、实现方案

1. Redis + Lua 固定窗口限流

1) 原理

  • 每个限流 key(如接口名、IP、用户 ID)在 Redis 里计数
  • 1 秒 / 1 分钟内最多允许 N 个请求
  • 超过就拒绝,没超过就计数 + 1

2) 适用场景:简单、高效、通用的分布式限流

    2. 滑动窗口限流(更精准)

    把1 秒拆成多个小时间片,只统计最近 1 秒内的所有请求,而不是按整秒重置。

    1) 原理

    • 用 ZSet 存储请求

      • key = 限流 key
      • member = 唯一请求 ID(UUID / 时间戳 + 随机数)
      • score = 当前时间戳(毫秒)
    • 每次请求做(原子):

      • 移除窗口外的旧数据
      • 统计窗口内总请求数
      • 判断是否超过阈值
      • 添加当前请求
    • 全部用 Lua 脚本 原子执行。

    2) 适用场景:精准度高,适合高并发核心接口

    3) 优点

    • 无临界突刺(比如最后 100ms 突增 100 请求),精准控制 QPS
    • 分布式全局精准限流
    • Redis + Lua 原子实现

    二、Spring Boot 实现固定窗口

    1. 依赖

    spring-boot-starter-data-redis

    2. 固定窗口

    1) 工具类

    @Component
    public class RedisRateLimit {
    
        @Autowired
        private StringRedisTemplate redisTemplate;
    
        private final DefaultRedisScript<Long> SCRIPT;
    
        public RedisRateLimit() {
            SCRIPT = new DefaultRedisScript<>();
            SCRIPT.setResultType(Long.class);
            SCRIPT.setScriptText("local count = redis.call('incr',KEYS[1]);\n" +
                    "if count == 1 then redis.call('expire',KEYS[1],ARGV[1]); end\n" +
                    "if count > tonumber(ARGV[2]) then return 0 else return 1 end");
        }
    
        /**
         * 分布式限流
         * @param key 限流key
         * @param seconds 窗口时间
         * @param maxCount 最大请求数
         * @return true=放行 false=限流
         */
        public boolean tryAcquire(String key, int seconds, int maxCount) {
            Long res = redisTemplate.execute(
                    SCRIPT,
                    Collections.singletonList(key),
                    String.valueOf(seconds),
                    String.valueOf(maxCount)
            );
            return res != null && res == 1;
        }
    }

    2) 使用

    @RestController
    public class TestController {
    
        @Autowired
        private RedisRateLimit redisRateLimit;
    
        @GetMapping("/api/test")
        public String test() {
            // 1秒内最多 10 个请求
            boolean allow = redisRateLimit.tryAcquire("limit:api:test", 1, 10);
            
            if (!allow) {
                return "系统繁忙,请稍后再试";
            }
            return "请求成功";
        }
    }

    3. 滑动窗口

    1) 工具类

    @Component
    public class RedisSlideWindowLimiter {
    
        private final StringRedisTemplate stringRedisTemplate;
        private final RedisScript<Long> script;
    
        public RedisSlideWindowLimiter(StringRedisTemplate stringRedisTemplate) {
            this.stringRedisTemplate = stringRedisTemplate;
    
            script = new DefaultRedisScript<>();
            script.setResultType(Long.class);
            script.setScriptText(
                "redis.call('ZREMRANGEBYSCORE',KEYS[1],0,tonumber(ARGV[3])-tonumber(ARGV[1]));\n"
                + "local count=redis.call('ZCARD',KEYS[1]);\n"
                + "if tonumber(count)>=tonumber(ARGV[2]) then return 0 end;\n"
                + "redis.call('ZADD',KEYS[1],tonumber(ARGV[3]),tonumber(ARGV[3]));\n"
                + "redis.call('EXPIRE',KEYS[1],tonumber(ARGV[1])/1000+1);\n"
                + "return 1"
            );
        }
    
        /**
         * 滑动窗口限流
         * @param key        限流key
         * @param windowMs   窗口大小(毫秒)
         * @param maxCount   最大请求数
         * @return 是否放行
         */
        public boolean tryAcquire(String key, long windowMs, int maxCount) {
            long now = System.currentTimeMillis();
    
            Long result = stringRedisTemplate.execute(
                    script,
                    Collections.singletonList(key),
                    String.valueOf(windowMs),
                    String.valueOf(maxCount),
                    String.valueOf(now)
            );
    
            return result != null && result == 1;
        }
    }

    2) 使用

    @RestController
    public class TestController {
    
        @Autowired
        private RedisSlideWindowLimiter slideWindowLimiter;
    
        @GetMapping("/api/test")
        public String test() {
            // 滑动窗口:1秒内最多10个请求(精准无突刺)
            boolean allow = slideWindowLimiter.tryAcquire("limit:api:test", 1000, 10);
            
            if (!allow) {
                return "限流了,请稍后再试";
            }
            return "请求成功";
        }
    }

    三、关键说明

    1. 为什么用 Redis?

    • 提供全局共享计数器
    • 性能极高,适合高并发

    2. 为什么必须用 Lua?

    • 不用 Lua:多台服务同时请求,会出现计数不准
    • 用 Lua:Redis 单线程执行,原子性,绝对不会超发

    3. 可以限流哪些维度?

    • 接口维度:limit:api:order
    • IP 维度:limit:ip:192.168.1.1
    • 用户维度:limit:uid:1001

    3. 限流 key 怎么设计?

    • 接口维度:rate_limit:api:getOrder
    • IP 维度:rate_limit:ip:192.168.1.1
    • 用户维度:rate_limit:uid:1001

    4. 为什么不用 Java 直接 get/set?

    • get + increment 不是原子操作,集群并发下会超限额。

    Logo

    北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

    更多推荐