后端_基于注解实现的请求限流

前言

在高并发场景下,恶意请求或突发流量可能导致系统过载,请求限流(Rate Limiting)通过控制单位时间内的请求次数,保护系统稳定运行。本文介绍声明式限流的实现原理与实战案例。

1、核心概念与注解说明

请求限流的核心是限制单位时间内的请求次数 ,通过 @RateLimiter 注解实现声明式限流,无需侵入业务代码。

1.1 @RateLimiter 注解参数

参数名 类型 说明 默认值
count int 单位时间内允许的最大请求次数 必传
time int 时间窗口大小(单位由 timeUnit 指定) 1
timeUnit TimeUnit 时间单位(如分钟、秒) TimeUnit.SECONDS
keyResolver Class<? extends KeyResolver> 限流维度解析器(如全局、用户ID、IP) DefaultRateLimiterKeyResolver
keyArg String 自定义限流Key的Spring EL表达式(配合ExpressionIdempotentKeyResolver使用)

1.2 限流维度解析器

支持多种限流粒度,通过 keyResolver 指定:

解析器类名 限流维度 适用场景
DefaultRateLimiterKeyResolver 全局级别 限制接口总请求量(如全局限 100次/分)
UserRateLimiterKeyResolver 用户ID级别 按用户限制(如单用户 10次/分)
ClientIpRateLimiterKeyResolver 客户端IP级别 按IP限制(如单IP 5次/分)
ServerNodeRateLimiterKeyResolver 服务器节点级别 集群中单个节点的请求限制
ExpressionIdempotentKeyResolver 自定义表达式 复杂维度(如 #user.id + '-' + #type

2、实现原理

限流功能基于 AOP 切面 + Redis 计数器 实现,核心流程如下:

  1. 拦截请求 :通过 RateLimiterAspect 切面拦截被 @RateLimiter 注解标记的方法。
  2. 生成限流Key :根据 keyResolver 解析限流维度(如用户ID),结合方法签名生成唯一Redis Key。
  3. 判断是否超限
    • 若未超限:Redis计数器+1,允许请求执行。
    • 若已超限:直接返回限流错误。
  4. 自动过期:Redis Key设置过期时间(与注解的时间窗口一致),自动清理过期计数。

核心原理图示

plain 复制代码
请求 → AOP切面拦截 → 生成限流Key → Redis计数检查
  ├─ 未超限 → 计数+1 → 执行方法
  └─ 已超限 → 返回429错误

3、实战案例

3.1 基础使用:全局限流

限制 /user/create 接口每分钟最多10次请求(所有用户共享):

java 复制代码
@RestController
public class UserController {

    @PostMapping("/user/create")
    // 每分钟最多10次请求(全局维度)
    @RateLimiter(count = 10, timeUnit = TimeUnit.MINUTES)
    public String createUser(@RequestBody User user) {
        userService.createUser(user); // 业务逻辑
        return "用户创建成功";
    }
}

3.2 进阶使用:按用户/IP限流

案例1:按用户ID限流

限制单个用户每小时最多5次修改个人信息:

java 复制代码
@PostMapping("/user/update")
// 单用户每小时最多5次请求(用户ID维度)
@RateLimiter(
    count = 5,
    time = 1,
    timeUnit = TimeUnit.HOURS,
    keyResolver = UserRateLimiterKeyResolver.class
)
public String updateUser(@RequestBody User user) {
    userService.updateUser(user);
    return "用户更新成功";
}
案例2:按IP限流

限制单个IP每分钟最多3次登录尝试(防暴力破解):

java 复制代码
@PostMapping("/login")
// 单IP每分钟最多3次请求(IP维度)
@RateLimiter(
    count = 3,
    timeUnit = TimeUnit.MINUTES,
    keyResolver = ClientIpRateLimiterKeyResolver.class
)
public String login(@RequestParam String username, @RequestParam String password) {
    return userService.login(username, password);
}
案例3:自定义维度限流

限制同一用户对同一商品的查询,每秒最多2次

java 复制代码
@GetMapping("/product/query")
// 自定义Key:用户ID+商品ID,每秒最多2次
@RateLimiter(
    count = 2,
    timeUnit = TimeUnit.SECONDS,
    keyResolver = ExpressionIdempotentKeyResolver.class,
    keyArg = "#userId + '-' + #productId" // Spring EL表达式
)
public Product queryProduct(Long userId, Long productId) {
    return productService.getById(productId);
}

3.3 限流效果

当请求超限后,接口返回标准化错误:

json 复制代码
{
  "code": 429,
  "data": null,
  "msg": "请求过于频繁,请稍后重试"
}

4、核心代码解析

4.1 AOP切面实现(RateLimiterAspect)

java 复制代码
/**
 * 拦截声明了 {@link RateLimiter} 注解的方法,实现限流操作
 *
 */
@Aspect
@Slf4j
public class RateLimiterAspect {

    /**
     * RateLimiterKeyResolver 集合
     */
    private final Map<Class<? extends RateLimiterKeyResolver>, RateLimiterKeyResolver> keyResolvers;

    private final RateLimiterRedisDAO rateLimiterRedisDAO;

    public RateLimiterAspect(List<RateLimiterKeyResolver> keyResolvers, RateLimiterRedisDAO rateLimiterRedisDAO) {
        this.keyResolvers = CollectionUtils.convertMap(keyResolvers, RateLimiterKeyResolver::getClass);
        this.rateLimiterRedisDAO = rateLimiterRedisDAO;
    }

    @Before("@annotation(rateLimiter)")
    public void beforePointCut(JoinPoint joinPoint, RateLimiter rateLimiter) {
        // 获得 IdempotentKeyResolver 对象
        RateLimiterKeyResolver keyResolver = keyResolvers.get(rateLimiter.keyResolver());
        Assert.notNull(keyResolver, "找不到对应的 RateLimiterKeyResolver");
        // 解析 Key
        String key = keyResolver.resolver(joinPoint, rateLimiter);

        // 获取 1 次限流
        boolean success = rateLimiterRedisDAO.tryAcquire(key,
                rateLimiter.count(), rateLimiter.time(), rateLimiter.timeUnit());
        if (!success) {
            log.info("[beforePointCut][方法({}) 参数({}) 请求过于频繁]", joinPoint.getSignature().toString(), joinPoint.getArgs());
            String message = StrUtil.blankToDefault(rateLimiter.message(),
                    GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getMsg());
            throw new ServiceException(GlobalErrorCodeConstants.TOO_MANY_REQUESTS.getCode(), message);
        }
    }

}

4.2 Redis计数实现(IdempotentRedisDAO)

java 复制代码
/**
 * 限流 Redis DAO
 */
@AllArgsConstructor
public class RateLimiterRedisDAO {

    /**
     * 限流操作
     *
     * KEY 格式:rate_limiter:%s // 参数为 uuid
     * VALUE 格式:String
     * 过期时间:不固定
     */
    private static final String RATE_LIMITER = "rate_limiter:%s";

    private final RedissonClient redissonClient;

    public Boolean tryAcquire(String key, int count, int time, TimeUnit timeUnit) {
        // 1. 获得 RRateLimiter,并设置 rate 速率
        RRateLimiter rateLimiter = getRRateLimiter(key, count, time, timeUnit);
        // 2. 尝试获取 1 个
        return rateLimiter.tryAcquire();
    }

    private static String formatKey(String key) {
        return String.format(RATE_LIMITER, key);
    }

    private RRateLimiter getRRateLimiter(String key, long count, int time, TimeUnit timeUnit) {
        String redisKey = formatKey(key);
        RRateLimiter rateLimiter = redissonClient.getRateLimiter(redisKey);
        long rateInterval = timeUnit.toSeconds(time);
        Duration duration = Duration.ofSeconds(rateInterval);
        // 1. 如果不存在,设置 rate 速率
        RateLimiterConfig config = rateLimiter.getConfig();
        if (config == null) {
            rateLimiter.trySetRate(RateType.OVERALL, count, duration);
            // 原因参见 https://t.zsxq.com/lcR0W
            rateLimiter.expire(duration);
            return rateLimiter;
        }
        // 2. 如果存在,并且配置相同,则直接返回
        if (config.getRateType() == RateType.OVERALL
                && Objects.equals(config.getRate(), count)
                && Objects.equals(config.getRateInterval(), TimeUnit.SECONDS.toMillis(rateInterval))) {
            return rateLimiter;
        }
        // 3. 如果存在,并且配置不同,则进行新建
        rateLimiter.setRate(RateType.OVERALL, count, duration);
        // 原因参见 https://t.zsxq.com/lcR0W
        rateLimiter.expire(duration);
        return rateLimiter;
    }

}

5、总结

基于 @RateLimiter 注解的限流方案具有以下优势:

  • 易用性:通过注解声明限流规则,无需编写复杂逻辑。
  • 灵活性:支持全局、用户、IP等多维度限流,满足不同场景。
  • 高性能:基于Redis原子操作,性能损耗低,支持分布式系统。

实际使用时,需根据业务场景合理设置 counttimeUnit(如高频接口设为秒级,低频接口设为分钟级),避免过度限流影响用户体验。

相关推荐
道可到4 小时前
百度面试真题 Java 面试通关笔记 04 |JMM 与 Happens-Before并发正确性的基石(面试可复述版)
java·后端·面试
飞快的蜗牛4 小时前
利用linux系统自带的cron 定时备份数据库,不需要写代码了
java·docker
聪明的笨猪猪4 小时前
Java Spring “IOC + DI”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
ThisIsMirror4 小时前
CompletableFuture并行任务超时处理模板
java·windows·python
珹洺5 小时前
Java-Spring入门指南(二十一)Thymeleaf 视图解析器
java·开发语言·spring
源码集结号5 小时前
一套智慧工地云平台源码,支持监管端、项目管理端,Java+Spring Cloud +UniApp +MySql技术开发
java·mysql·spring cloud·uni-app·源码·智慧工地·成品系统
EnCi Zheng5 小时前
Spring Security 最简配置完全指南-从入门到精通前后端分离安全配置
java·安全·spring
程序员小假5 小时前
为什么这些 SQL 语句逻辑相同,性能却差异巨大?
java·后端
泉城老铁6 小时前
springboot实现对接poi 导出excel折线图
java·spring boot·后端