SpringBoot整合Redis实现分布式限流功能

 更新时间:2026年07月03日 08:43:45   作者:张老师技术栈  
线上服务最怕被恶意刷接口或者突发流量把系统打垮,单机限流用 Guava RateLimiter 就行,但微服务多实例部署时必须用分布式限流——所有实例共享同一个限流状态,因此本文给大家介绍了SpringBoot整合Redis实现分布式限流功能,需要的朋友可以参考下

引言

线上服务最怕被恶意刷接口或者突发流量把系统打垮。单机限流用 Guava RateLimiter 就行,但微服务多实例部署时必须用分布式限流——所有实例共享同一个限流状态。

一、限流的常见算法

算法原理特点
固定窗口1 分钟内最多 N 个请求简单但有突刺问题
滑动窗口把窗口切小段,更平滑推荐,较常用
漏桶请求进桶,匀速流出平滑流量,但突发处理慢
令牌桶匀速放令牌,拿到的才通过允许一定突发,最均衡

Redis 实现限流的本质: 用 Redis 计数器/有序集合记录请求次数,加上过期时间自动清理。

二、固定窗口限流

最简单的方案:一个 key 记录 1 分钟内的请求数。

@Component
public class FixedWindowRateLimiter {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 固定窗口限流
     * @param key   限流 key(如 "rate:user:1001")
     * @param max    窗口内最大请求数
     * @param window 窗口大小(秒)
     * @return true 通过,false 被限
     */
    public boolean tryAcquire(String key, long max, long window) {
        // INCR 原子自增
        Long count = redisTemplate.opsForValue().increment(key);
        if (count == 1) {
            // 第一次访问,设置过期时间
            redisTemplate.expire(key, window, TimeUnit.SECONDS);
        }
        return count <= max;
    }
}
@RestController
public class TestController {

    @Autowired
    private FixedWindowRateLimiter rateLimiter;

    @GetMapping("/api/test")
    public ResultVO<?> test() {
        // 限制每个用户每秒最多 5 次
        if (!rateLimiter.tryAcquire("rate:user:1001", 5, 1)) {
            return ResultVO.error(429, "请求太频繁,请稍后");
        }
        return ResultVO.success("成功");
    }
}

问题: 窗口边界会有突刺——比如第 59 秒和第 61 秒各发 5 个请求,理论上通过 10 个,但看起来像 1 秒内 10 个。

三、滑动窗口限流(推荐)

用 Redis ZSet(有序集合)记录每个请求的时间戳,滑动窗口更精确。

@Component
public class SlidingWindowRateLimiter {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 滑动窗口限流
     * @param key      限流 key
     * @param max      窗口内最大请求数
     * @param window   窗口大小(毫秒)
     * @return true 通过,false 被限
     */
    public boolean tryAcquire(String key, long max, long windowMs) {
        long now = System.currentTimeMillis();
        long windowStart = now - windowMs;

        // 使用 Lua 脚本保证原子性
        String luaScript =
            "redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1]) " +    // 移除窗口外的记录
            "local count = redis.call('ZCARD', KEYS[1]) " +             // 统计窗口内请求数
            "if count < tonumber(ARGV[2]) then " +
            "  redis.call('ZADD', KEYS[1], ARGV[3], ARGV[3]) " +        // 加入当前请求
            "  redis.call('EXPIRE', KEYS[1], ARGV[4]) " +               // 设置过期(秒)
            "  return 1 " +
            "else " +
            "  return 0 " +
            "end";

        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(luaScript, Long.class),
            Collections.singletonList(key),
            String.valueOf(windowStart),
            String.valueOf(max),
            String.valueOf(now),
            String.valueOf(windowMs / 1000 + 1)
        );

        return Long.valueOf(1).equals(result);
    }
}

四、注解式限流

把限流逻辑封装成注解,用在需要的接口上,代码更干净。

1. 定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {

    /** 限流 key(支持 SpEL) */
    String key() default "";

    /** 限流维度:用户、IP、接口 */
    String type() default "user";  // user、ip、api

    /** 最大请求数 */
    long max() default 10;

    /** 窗口大小(秒) */
    long window() default 1;
}

2. 实现 AOP 切面

@Aspect
@Component
public class RateLimitAspect {

    @Autowired
    private SlidingWindowRateLimiter rateLimiter;

    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
        // 构建 key
        String key = buildKey(rateLimit, pjp);

        // 执行限流
        boolean acquired = rateLimiter.tryAcquire(
                key, rateLimit.max(), rateLimit.window() * 1000);

        if (!acquired) {
            throw new RuntimeException("请求太频繁,请稍后重试");
        }

        return pjp.proceed();
    }

    private String buildKey(RateLimit rateLimit, ProceedingJoinPoint pjp) {
        String prefix = "rate:";

        switch (rateLimit.type()) {
            case "ip":
                // 从请求中获取 IP(需要注入 RequestContextHolder)
                HttpServletRequest request = ((ServletRequestAttributes)
                    RequestContextHolder.getRequestAttributes()).getRequest();
                String ip = request.getRemoteAddr();
                return prefix + "ip:" + ip;

            case "api":
                // 按接口限流
                String methodName = pjp.getSignature().toShortString();
                return prefix + "api:" + methodName;

            default:
                // 按用户限流(需要从 Token 获取用户 ID)
                String userId = StpUtil.getLoginIdAsString();
                return prefix + "user:" + userId;
        }
    }
}

// 全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(RuntimeException.class)
    public ResultVO<?> handle(RuntimeException e) {
        if (e.getMessage().contains("请求太频繁")) {
            return ResultVO.error(429, "请求太频繁,请稍后重试");
        }
        return ResultVO.error(500, e.getMessage());
    }
}

3. 使用

@RestController
@RequestMapping("/api")
public class OrderController {

    @PostMapping("/order")
    @RateLimit(key = "createOrder", type = "user", max = 5, window = 10)
    public ResultVO<?> createOrder(@RequestBody OrderDTO dto) {
        // 每个用户 10 秒内最多 5 次下单
        return ResultVO.success("下单成功");
    }

    @GetMapping("/product/list")
    @RateLimit(type = "api", max = 100, window = 1)
    public ResultVO<?> list() {
        // 接口级别限流:每秒最多 100 次
        return ResultVO.success("商品列表");
    }
}

五、秒杀系统的限流

在秒杀系统中,限流是保护系统的第一道防线:

@RestController
@RequestMapping("/api/seckill")
public class SeckillController {

    @Autowired
    private SeckillService seckillService;

    @PostMapping("/do/{productId}")
    @RateLimit(type = "user", max = 1, window = 10)    // 每个用户10秒只能秒杀1次
    public ResultVO<?> doSeckill(@PathVariable Long productId) {
        long userId = StpUtil.getLoginIdAsLong();
        return seckillService.doSeckill(productId, userId, "用户");
    }
}

秒杀系统的多层防护:

用户请求
  ↓
① 限流(Redis 滑动窗口)→ 被限返回"操作频繁"
  ↓
② 参数校验 → 不合法直接返回
  ↓
③ 时间校验 → 没开始/已结束直接返回
  ↓
④ 重复提交校验(SETNX)→ 已提交直接返回
  ↓
⑤ Redis 预减库存 → 库存不足直接返回
  ↓
⑥ 数据库扣库存(乐观锁)→ 失败回滚
  ↓
⑦ 生成订单

限流在最外层,把大部分无效请求挡在门外。

六、IP 限流

@GetMapping("/api/public")
@RateLimit(type = "ip", max = 30, window = 60)
public ResultVO<?> publicApi() {
    // 同一 IP 每分钟最多 30 次
    return ResultVO.success("公开接口");
}

获取真实 IP(经过 Nginx 代理时):

public static String getRealIp(HttpServletRequest request) {
    String ip = request.getHeader("X-Forwarded-For");
    if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getHeader("X-Real-IP");
    }
    if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getRemoteAddr();
    }
    // 多个代理时取第一个
    if (ip != null && ip.contains(",")) {
        ip = ip.split(",")[0].trim();
    }
    return ip;
}

七、限流配置管理

把限流参数放在配置中心或 yml 中,不用改代码:

rate-limit:
  defaults:
    max: 10
    window: 1
  rules:
    - key: "createOrder"
      max: 5
      window: 10
    - key: "seckill"
      max: 1
      window: 10
    - key: "publicApi"
      max: 30
      window: 60
@ConfigurationProperties(prefix = "rate-limit")
@Data
public class RateLimitProperties {
    private Rule defaults = new Rule();
    private List<Rule> rules = new ArrayList<>();

    @Data
    public static class Rule {
        private String key;
        private long max;
        private long window;
    }
}

八、限流效果验证

# 快速请求测试限流效果
for i in {1..20}; do
  curl -X POST http://localhost:9090/api/order
  echo ""
done

# 正常返回:{"code":200,"message":"下单成功"}
# 被限返回:{"code":429,"message":"请求太频繁,请稍后重试"}

九、限流 vs 熔断 vs 降级

限流 Ratelimit熔断 CircuitBreaker降级 Degrade
作用控制请求速率防止故障扩散舍弃非核心功能
时机请求进来时错误率超标时资源不足时
工具Redis + LuaSentinel、Resilience4jSentinel、Hystrix

建议: 限流是必须的,熔断和降级在大流量的核心系统上建议加上。

以上就是SpringBoot整合Redis实现分布式限流功能的详细内容,更多关于SpringBoot Redis分布式限流的资料请关注脚本之家其它相关文章!

相关文章

  • Mybatis实现批量删除和多条件查询方式

    Mybatis实现批量删除和多条件查询方式

    本文介绍了批量删除功能的实现步骤,包括后台代码的修改,具体涉及到mapper层、Service层、Controller接口等层面的修改,同时也介绍了多条件查询功能的实现,具体包括创建封装查询条件的类,mapper层、Service层、Controller接口的修改等
    2026-05-05
  • Java8如何优雅的记录代码运行时间

    Java8如何优雅的记录代码运行时间

    这篇文章主要为大家详细介绍了 Java 8 中几种记录代码运行时间的优雅方式,并附上实用工具类与建议,希望可以帮助大家提高大家的代码可读性与复用性
    2025-04-04
  • Java IO流对文件File操作

    Java IO流对文件File操作

    这篇文章主要介绍了Java IO流对文件File操作,java封装的一个操作文件及文件夹(目录)的对象。可以操作磁盘上的任何一个文件和文件夹
    2022-12-12
  • Java高级用法中的JNA类型映射注意细节及使用问题

    Java高级用法中的JNA类型映射注意细节及使用问题

    本文介绍了在使用JNA方法映射中应该注意的一些细节和具体的使用问题,对java  JNA类型映射注意细节感兴趣的朋友一起看看吧
    2022-04-04
  • Java数组动态扩容的实现示例

    Java数组动态扩容的实现示例

    本文主要介绍了Java数组动态扩容的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2025-12-12
  • 基于Java ActiveMQ的实例讲解

    基于Java ActiveMQ的实例讲解

    下面小编就为大家带来一篇基于Java ActiveMQ的实例讲解。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-09-09
  • Java之idea @NotNull @Nullable 注解使用

    Java之idea @NotNull @Nullable 注解使用

    这篇文章主要介绍了Java之idea @NotNull @Nullable 注解使用,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-01-01
  • SpringBoot AOP如何配置全局事务

    SpringBoot AOP如何配置全局事务

    这篇文章主要介绍了SpringBoot AOP如何配置全局事务问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-05-05
  • Java实现双色球抽奖随机算法示例

    Java实现双色球抽奖随机算法示例

    本篇文章主要介绍了Java实现双色球抽奖随机算法示例,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-06-06
  • jdk中动态代理异常处理分析:UndeclaredThrowableException

    jdk中动态代理异常处理分析:UndeclaredThrowableException

    最近在工作中遇到了报UndeclaredThrowableException的错误,通过查找相关的资料,终于解决了,所以这篇文章主要给大家介绍了关于jdk中动态代理异常处理分析:UndeclaredThrowableException的相关资料,需要的朋友可以参考下
    2018-04-04

最新评论