当前位置: 首页 > news >正文

java每日精进 5.29【请求限流】

在高并发场景下,频繁请求可能导致系统过载或资源耗尽,例如用户快速点击按钮引发大量请求。

项目通过ratelimiter 模块提供了声明式限流功能,利用 @RateLimiter 注解和 Redis 实现分布式限流。

本文将深入剖析  项目中限流的实现原理,回答如何按用户或 IP 限制请求,指导如何在新项目中嵌入限流,并对比幂等性与限流的异同及适用场景。

项目使用 @RateLimiter 注解,通过 Redisson 和 AOP 实现分布式限流,限制指定时间段内的请求次数。以下以 /admin-api/system/user/page 接口为例,详细解析实现过程,限制每分钟 10 次请求。

实现步骤

步骤 1:引入依赖

在模块的 pom.xml 中添加 yudao spring-boot-starter-protection 依赖:

<dependency><groupId>cn.iocoder.boot</groupId><artifactId>yudao spring-boot-starter-protection</artifactId></dependency>

解释

  • yudao spring-boot-starter-protection 提供限流和幂等功能的模块,依赖 Redisson 和 Spring Data Redis。
  • 自动配置 Redis 客户端,简化集成。
步骤 2:配置 Redis

在 application.yml 中配置 Redis 连接:

spring:redis:host: 127.0.0.1port: 6379database: 0

解释

  • 限流依赖 Redis 存储计数信息,确保分布式环境一致性。
  • 使用 RedissonClient 操作 Redis。
步骤 3:定义 @RateLimiter 注解

@RateLimiter 注解用于标记需要限流的方法,定义如下:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiter {int time() default 1;TimeUnit timeUnit() default TimeUnit.SECONDS;int count() default 100;String message() default "";Class<? extends RateLimiterKeyResolver> keyResolver() default DefaultRateLimiterKeyResolver.class;String keyArg() default "";
}

解释

  • 属性
    • time 和 timeUnit:限流时间窗口,默认 1 秒。
    • count:时间窗口内的最大请求次数,默认 100。
    • message:超限时的错误提示,默认为 TOO_MANY_REQUESTS 消息。
    • keyResolver:键解析器,支持 DefaultRateLimiterKeyResolver(全局)、UserRateLimiterKeyResolver(用户)、ClientIpRateLimiterKeyResolver(IP)等。
    • keyArg:配合 ExpressionIdempotentKeyResolver 的 SpEL 表达式。
  • 作用:标记方法,触发 AOP 切面处理。
步骤 4:实现键解析器

Yudao 提供多种键解析器,DefaultRateLimiterKeyResolver 是默认实现:

/*** 默认(全局级别)限流 Key 解析器,使用方法名 + 方法参数,组装成一个 Key** 为了避免 Key 过长,使用 MD5 进行“压缩”*/
public class DefaultRateLimiterKeyResolver implements RateLimiterKeyResolver {@Overridepublic String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {String methodName = joinPoint.getSignature().toString();String argsStr = StrUtil.join(",", joinPoint.getArgs());return SecureUtil.md5(methodName + argsStr);}}

解释

  • 生成键:基于方法名和参数的 MD5 哈希,确保全局唯一。
  • 其他解析器
    • UserRateLimiterKeyResolver:基于用户 ID(如从 Security 上下文获取)。
    • ClientIpRateLimiterKeyResolver:基于客户端 IP。
步骤 5:实现 Redis 操作

RateLimiterRedisDAO 封装 Redis 限流操作,使用 Redisson 的 RRateLimiter:

@AllArgsConstructor
public class RateLimiterRedisDAO {// Redis键格式,用于存储限流器配置private static final String RATE_LIMITER = "rate_limiter:%s";// Redisson客户端,用于操作Redis分布式对象private final RedissonClient redissonClient;/*** 尝试获取令牌,判断请求是否允许通过限流* @param key 限流键,例如用户ID、IP等* @param count 时间窗口内允许的最大请求数* @param time 时间窗口大小* @param timeUnit 时间单位* @return 是否允许通过(true:允许;false:拒绝)*/public Boolean tryAcquire(String key, int count, int time, TimeUnit timeUnit) {// 获取或配置分布式限流器RRateLimiter rateLimiter = getRRateLimiter(key, count, time, timeUnit);// 尝试获取1个令牌(非阻塞操作)return rateLimiter.tryAcquire();}/*** 格式化Redis键* @param key 业务键(如用户ID)* @return 格式化后的Redis键(如rate_limiter:user123)*/private static String formatKey(String key) {return String.format(RATE_LIMITER, key);}/*** 获取或配置分布式限流器* @param key 限流键* @param count 令牌生成速率(每秒生成的令牌数)* @param time 时间窗口大小* @param timeUnit 时间单位* @return 配置好的限流器*/private RRateLimiter getRRateLimiter(String key, long count, int time, TimeUnit timeUnit) {// 格式化Redis键String redisKey = formatKey(key);// 从Redisson获取限流器(若不存在则创建空壳)RRateLimiter rateLimiter = redissonClient.getRateLimiter(redisKey);// 将时间窗口转换为秒long rateInterval = timeUnit.toSeconds(time);// 获取现有配置(首次调用时为null)RateLimiterConfig config = rateLimiter.getConfig();// 分支1:限流器不存在,需要初始化配置if (config == null) {// 设置限流规则:全局限流模式,固定速率生成令牌rateLimiter.trySetRate(RateType.OVERALL, count, rateInterval, RateIntervalUnit.SECONDS);// 设置限流器的过期时间(避免冷数据长期占用内存)rateLimiter.expire(rateInterval, TimeUnit.SECONDS);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, rateInterval, RateIntervalUnit.SECONDS);// 重置过期时间rateLimiter.expire(rateInterval, TimeUnit.SECONDS);return rateLimiter;}
}

解释

  • 键格式:rate_limiter:{key},如 rate_limiter:md5(method+args)。
  • tryAcquire:尝试获取 1 次限流配额。
  • RRateLimiter:Redisson 的限流器,设置速率(如 10 次/分钟),并配置过期时间。
步骤 6:实现 AOP 切面

RateLimiterAspect 拦截 @RateLimiter 方法:

/*** 拦截声明了 {@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) {// 获得 RateLimiterKeyResolver(不同组合的 MD5 )对象RateLimiterKeyResolver keyResolver = keyResolvers.get(rateLimiter.keyResolver());Assert.notNull(keyResolver, "找不到对应的 RateLimiterKeyResolver");// 解析 KeyString 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);}}}

解释

  • 解析键:使用指定的 keyResolver 生成键。
  • 检查限流:通过 RateLimiterRedisDAO.tryAcquire 判断是否超限。
  • 异常抛出:超限时抛出 ServiceException(code: 429)。
步骤 7:应用 @RateLimiter

在 UserController 的 /page 接口上使用:

@RestController
@RequestMapping("/system/user")
public class UserController {@GetMapping("/page")@RateLimiter(count = 1, time = 60)public CommonResult<PageResult<UserRespVO>> getUserPage(@Valid UserPageReqVO pageReqVO) {// 模拟分页逻辑return CommonResult.success(new PageResult<>());}
}

想换用其他MD5,直接在注解上修改即可

@RateLimiter(count = 10, timeUnit = TimeUnit.MINUTES, keyResolver = UserRateLimiterKeyResolver.class)

/*** 用户级别的限流 Key 解析器,使用方法名 + 方法参数 + userId + userType,组装成一个 Key** 为了避免 Key 过长,使用 MD5 进行“压缩”*/
public class UserRateLimiterKeyResolver implements RateLimiterKeyResolver {@Overridepublic String resolver(JoinPoint joinPoint, RateLimiter rateLimiter) {String methodName = joinPoint.getSignature().toString();String argsStr = StrUtil.join(",", joinPoint.getArgs());Long userId = WebFrameworkUtils.getLoginUserId();Integer userType = WebFrameworkUtils.getLoginUserType();return SecureUtil.md5(methodName + argsStr + userId + userType);}}

http://www.xdnf.cn/news/717103.html

相关文章:

  • 7-Zip 工具使用
  • How to Initiate Back-to-Back Write Transactions from Master
  • DMBOK对比知识点整理(4)
  • 力扣HOT100之动态规划:118. 杨辉三角
  • 今日分享:怎么综合分析5星股票?
  • 【Unity博客节选】Playable Graph Monitor 安装使用
  • 安全帽检测算法AI智能分析网关V4守护工地/矿山/工厂等多场景作业安全
  • Accelerate实现多卡并行训练
  • Nexus仓库数据高可用备份与恢复方案(上)
  • MVCC(多版本并发控制)机制
  • Cangjie 中的值类型与引用类型
  • 设置变体控制两个apk, 一个是有密码,一个是没有密码!
  • 英语写作中“广泛、深入、详细地(的)”extensively、in-depth、at length (comprehensive )的用法
  • 46. Permutations和47. Permutations II
  • Spring Event(事件驱动机制)
  • 力扣面试150题--二叉树的右视图
  • leetcode hot100刷题日记——27.对称二叉树
  • ubuntu系统上运行jar程序输出时间时区不对
  • C#实现单实例应用程序:确保程序唯一运行实例
  • 算法第32天|509. 斐波那契数、70. 爬楼梯、746. 使用最小花费爬楼梯
  • 构筑电网“无形防线”: 防外破告警在线监测服务系统
  • 如何批量给局域网内网里的电脑发送信息
  • STM32 HAL库函数学习 GPIO篇
  • 【Redis】RDB和AOF混合使用
  • Java求职面试:从核心技术到AI与大数据的全面考核
  • 网络编程之网络编程预备知识
  • Python对接GPT-4o API接口:聊天与文件上传功能详解
  • 人工智能浪潮下,制造企业如何借力DeepSeek实现数字化转型?
  • cutlass学习教程
  • Security