Redis实现分布式限流

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

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

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

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

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

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

一、实现方案

1. Redis + Lua 固定窗口限流

  1. 原理
  • 每个限流 key(如接口名、IP、用户 ID)在 Redis 里计数
  • 1 秒 / 1 分钟内最多允许 N 个请求
  • 超过就拒绝,没超过就计数 + 1
  1. 适用场景:简单、高效、通用的分布式限流

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

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

  1. 原理
  • ZSet 存储请求

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

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

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

  2. 优点

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

二、Spring Boot 实现固定窗口

1. 依赖

spring-boot-starter-data-redis

2. 固定窗口

  1. 工具类
java 复制代码
@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;
    }
}
  1. 使用
java 复制代码
@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. 工具类
java 复制代码
@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;
    }
}
  1. 使用
java 复制代码
@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?
  • 提供全局共享计数器
  • 性能极高,适合高并发
  1. 为什么必须用 Lua?
  • 不用 Lua:多台服务同时请求,会出现计数不准
  • 用 Lua:Redis 单线程执行,原子性,绝对不会超发
  1. 可以限流哪些维度?
  • 接口维度:limit:api:order
  • IP 维度:limit:ip:192.168.1.1
  • 用户维度:limit:uid:1001
  1. 限流 key 怎么设计?
  • 接口维度:rate_limit:api:getOrder
  • IP 维度:rate_limit:ip:192.168.1.1
  • 用户维度:rate_limit:uid:1001
  1. 为什么不用 Java 直接 get/set?
  • get + increment 不是原子操作,集群并发下会超限额。
相关推荐
woniu_buhui_fei15 小时前
分布式限流
java·分布式
杜子不疼.15 小时前
从“能用“到“敢用“:DolphinDB 通过国家安全可靠测评,时序数据库国产替代迈入新阶段
数据库·oracle·时序数据库
sbjdhjd15 小时前
从 0 到 1 构建高可用企业级 NoSql 数据库 Redis 集群
linux·运维·redis·云原生·kubernetes·开源·云计算
量子-Alex15 小时前
【大模型智能体】A practical guide to building agents
大数据·数据库·人工智能
YOU OU15 小时前
Spring事务和事务传播机制
java·数据库·spring
向日的葵00615 小时前
Redis后端分布式与高并发架构演进
redis·分布式·架构
闪电悠米15 小时前
黑马点评-优惠券秒杀-01_redis_global_id
数据库·redis·缓存
牧羊狼的狼16 小时前
MySQL 提升SQL查询性能的全套实战优化方法
数据库·sql·mysql
weixin_4896900216 小时前
企业微信 PC 端本地数据库结构中的巧妙设计
数据库·oracle·企业微信