赞
踩
本文主要介绍了SpringBoot集成Redisson实现限流,主要涉及到的类为Redisson
中的org.redisson.api.RRateLimiter
,其实现的是令牌桶
限流
<!--redisson依赖-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.0</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</exclusion>
</exclusions>
</dependency>
redisson相关参数配置请参考:springboot集成redisson
@Documented @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimiter { /** * * @return */ LimitType limitType() default LimitType.DEFAULT; /** * 限流key,支持使用Spring el表达式来动态获取方法上的参数值 * 格式类似于 #code.id #{#code} */ String key() default ""; /** * 单位时间产生的令牌数,默认100 */ long rate() default 100; /** * 时间间隔,默认1秒 */ long rateInterval() default 1; /** * 拒绝请求时的提示信息 */ String showPromptMsg() default "服务器繁忙,请稍候再试"; }
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiters {
RateLimiter[] value();
}
public enum LimitType {
DEFAULT, // 全局
IP, // 根据客户端ip地址限制
CLUSTER // 对应服务器实例
}
@Slf4j @Aspect public class RateLimiterAspect { private static final String RATE_LIMIT_KEY = "ratelimiter:"; /** * 定义spel表达式解析器 */ private final ExpressionParser parser = new SpelExpressionParser(); /** * 方法参数解析器 */ private final ParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer(); @Before("@annotation(rateLimiter)") public void doBefore(JoinPoint point, RateLimiter rateLimiter) { isAllow(point, rateLimiter); } @Before("@annotation(rateLimiters)") public void doBefore(JoinPoint point, RateLimiters rateLimiters) { } private void isAllow(JoinPoint point, RateLimiter rateLimiter) { long rateInterval = rateLimiter.rateInterval(); long rate = rateLimiter.rate(); String combineKey = getCombineKey(rateLimiter, point); RateType rateType = RateType.OVERALL; if (rateLimiter.limitType() == LimitType.CLUSTER) { rateType = RateType.PER_CLIENT; } long number = RedisUtil.rateLimiter(combineKey, rateType, rate, rateInterval); if (number == -1) { String message = rateLimiter.showPromptMsg(); throw new RuntimeException(message); } log.info("限制令牌 => {}, 剩余令牌 => {}, 缓存key => '{}'", rate, number, combineKey); } /** * 获取rateLimite对应的复合型key值 * @param rateLimiter * @param point * @return */ private String getCombineKey(RateLimiter rateLimiter, JoinPoint point) { StringBuilder stringBuffer = new StringBuilder(RATE_LIMIT_KEY); String key = rateLimiter.key(); Method method = getMethod(point); // 获取URI,然后计算md5 ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = requestAttributes.getRequest(); String uniqueKey = DigestUtils.md5DigestAsHex(request.getRequestURI().getBytes(StandardCharsets.UTF_8)); stringBuffer.append(uniqueKey); String ip = ServletUtil.getClientIP(request); // 判断是否是spel格式 if (StrUtil.containsAny(key, "#")) { EvaluationContext context = new MethodBasedEvaluationContext(null, method, point.getArgs(), discoverer); try { Object value = parser.parseExpression(key).getValue(context); if (value != null) { key = value.toString(); } } catch (Exception e) { log.error("【{}】请求,线程:【{}】,获取spel数据失败:{}", ip, Thread.currentThread().getName(), e.getMessage()); } } if (rateLimiter.limitType() == LimitType.IP) { // 获取请求ip stringBuffer.append(":").append(ip); } // key名称 if (!StrUtil.hasBlank(key)) { stringBuffer.append(":").append(key); } return stringBuffer.toString(); } /** * 获取切面方法对象 * * @param point * @return */ private Method getMethod(JoinPoint point) { MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); if (method.getDeclaringClass().isInterface()) { try { method = point.getTarget().getClass().getDeclaredMethod(signature.getName(), method.getParameterTypes()); } catch (NoSuchMethodException e) { log.error(null, e); } } return method; } }
@NoArgsConstructor(access = AccessLevel.PRIVATE) public class RedisUtil { private volatile static RedissonClient redissonClient; public static void setRedissonClient(RedissonClient redissonClient) { RedisUtil.redissonClient = redissonClient; } public static RedissonClient getRedissonClient() { return redissonClient; } // =========================缓存-限流器开始============================= /** * 令牌桶限流(整流器) * * @param key 限流key * @param rateType 限流类型 * @param rate 速率 * @param rateInterval 速率间隔 * @return -1 表示失败 */ public static long rateLimiter(String key, RateType rateType, long rate, long rateInterval) { RRateLimiter rateLimiter = redissonClient.getRateLimiter(key); rateLimiter.trySetRate(rateType, rate, rateInterval, RateIntervalUnit.SECONDS); if (rateLimiter.tryAcquire()) { return rateLimiter.availablePermits(); } else { return -1L; } } // =========================缓存-限流器结束============================= }
@RestController public class SsoController { @RequestMapping("/hello") @ResponseBody @WebLog(value = "请求数据") @RateLimiter(limitType = LimitType.IP, rate = 10, rateInterval = 100) public String hello(@RequestParam(name = "name", defaultValue = "unknown user") String name) { return "Hello "; } // http://127.0.0.1:8082/login @PostMapping(value = "/login", consumes = MediaType.APPLICATION_JSON_VALUE) @RateLimiter(key = "#user.name", limitType = LimitType.IP, rate = 10, rateInterval = 100) public String login(@Valid @RequestBody User user) { return "Hello "; } }
使用Redisson存在的缺陷
使用Redisson实现限流时,生成的key不会自动过期,需要配合redis删除策略或者手动清除
优化
案例中限流规则都是配置到接口上,不能随时调整;可以通过系统配置保存到缓存中,然后在aop中获取对应的限流规则就能实现通过页面操作控制限流。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。