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 不是原子操作,集群并发下会超限额。
相关推荐
爱和冰阔落20 小时前
【MySQL 慢查询排查实战】列表接口逐渐变慢时,怎样从请求链路定位原因
android·数据库·mysql
JacksonMx20 小时前
企业级 JPA 与数据库架构落地指南
数据库·数据库架构
Dxy123931021620 小时前
MySQL有哪些锁?InnoDB锁体系完整梳理博客
数据库·mysql
2602_9599609220 小时前
电商大厂Java面试实录:Spring Boot/JVM/Redis/Kafka/微服务/安全/测试全解析
java·jvm·spring boot·redis·面试题
Wang's Blog21 小时前
PostgreSQL笔记38:慢查询定位与优化的系统方法论
数据库·笔记·postgresql
天涯明月199321 小时前
ray深度研究报告
大数据·人工智能·分布式·ray
幻风_huanfeng21 小时前
软考:高级软件架构师学习笔记----数据库
数据库·软考·系统架构师
小的~~21 小时前
面试被问懵了?为什么 Redis 单线程还能保证 Lua 脚本原子性,却偏偏选了 Lua?
redis·面试·lua
衿心.21 小时前
Django TemplateDoesNotExist
数据库·django·sqlite
方便面不加香菜1 天前
MySQL 复合查询
数据库·mysql