RRateLimiter的使用
Redisson 的 RRateLimiter 是一个分布式限流器,用于在分布式系统中控制访问速率。
基本用法
导入jar
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>
配置类
package com.qfedu.ratelimit.config;import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class RedissonConfig {@Beanpublic RedissonClient getRedisson() {Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379").setRetryInterval(5000).setTimeout(10000).setConnectTimeout(10000);return Redisson.create(config);}
}
通过控制器测试
@RestController
public class TestController {@Resourceprivate RedissonClient redissonClient;@GetMapping("/test")public String test() {// 根据key获取限流器实例RRateLimiter rateLimiter = redissonClient.getRateLimiter("limit:1");// 初始化限流器,本例表示5秒内产生3个许可 rateLimiter.trySetRate(RateType.OVERALL, 3, 5, RateIntervalUnit.SECONDS);// 尝试获取许可,获取到返回true,否则返回falseboolean ret1 = rateLimiter.tryAcquire(1);System.out.println(ret1);boolean ret2 = rateLimiter.tryAcquire(1);System.out.println(ret2);boolean ret3 = rateLimiter.tryAcquire(1);System.out.println(ret3);// 第四次获取许可,返回false,表示不能访问boolean ret4 = rateLimiter.tryAcquire(1);System.out.println(ret4);return "success";}}
关于trySetRate的说明
其中trySetRaet()的第一个参数表示RateType有两个值:
RateType.OVERALL
: 全局限流(所有实例共享)RateType.PER_CLIENT
: 按客户端限流(需要配置客户端标识)
第二个参数表示限流的最大次数
第三个参数表示限流的时间间隔
第四个参数表示限流的时间间隔的单位
通过查看源码,其内部执行了如下的lua脚本
redis.call('hsetnx', KEYS[1], 'rate', ARGV[1]);
redis.call('hsetnx', KEYS[1], 'interval', ARGV[2]);
return redis.call('hsetnx', KEYS[1], 'type', ARGV[3]);
其内部使用hsetnx,如果对应的key和field存在,则不会创建field
通过自定义注解和AOP进行封装
工具类
package com.qfedu.ratelimit.aspect;import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;public class IpAddressUtil {/*** 获取客户端真实IP地址*/public static String getClientIpAddress() {ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();String[] headers = {"X-Forwarded-For","X-Real-IP","Proxy-Client-IP","WL-Proxy-Client-IP","HTTP_CLIENT_IP","HTTP_X_FORWARDED_FOR"};// 1. 检查各种代理头信息for (String header : headers) {String ip = request.getHeader(header);if (isValidIp(ip)) {return getFirstIp(ip);}}// 2. 直接获取远程地址String remoteAddr = request.getRemoteAddr();if (isValidIp(remoteAddr)) {return remoteAddr;}throw new RuntimeException("无法获取远程ip地址");}/*** 验证IP地址是否有效*/private static boolean isValidIp(String ip) {return ip != null &&!ip.isEmpty() &&!"unknown".equalsIgnoreCase(ip) &&!"0:0:0:0:0:0:0:1".equals(ip) &&!"127.0.0.1".equals(ip);}/*** 处理多个IP的情况(如X-Forwarded-For: client, proxy1, proxy2)*/private static String getFirstIp(String ip) {if (ip.contains(",")) {String[] ips = ip.split(",");for (String singleIp : ips) {String trimmedIp = singleIp.trim();if (isValidIp(trimmedIp)) {return trimmedIp;}}}return ip.trim();}
}
package com.qfedu.ratelimit.aspect;import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;public class TokenUtils {/*** 通过token获取用户id** @return*/public static Integer getUidFromToken() {// 动态获取HttpServletRequest对象ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();String token = request.getHeader("Authorization");
// if (token == null || token.isEmpty()) {
// throw new RuntimeException("请重新登录");
// }
// Claims claims = JwtUtils.parseJWT(token);
// Object uid = claims.get("uid");
// return Integer.valueOf(uid.toString());// 本例为了方便测试,写死返回的用户idreturn 123;}
}
枚举
package com.qfedu.ratelimit.aspect;public enum RateLimitTypeEnum {TYPE_IP(1, "根据IP限流"),TYPE_USER(2, "根据用户限流");private int type;private String desc;RateLimitTypeEnum(int type, String desc) {this.type = type;this.desc = desc;}
}
自定义注解
package com.qfedu.ratelimit.aspect;import org.redisson.api.RateIntervalUnit;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {RateLimitTypeEnum type() default RateLimitTypeEnum.TYPE_USER;// String key() default "";long rate() default 10;long interval() default 60;RateIntervalUnit unit() default RateIntervalUnit.SECONDS;String message() default "请求过于频繁,请稍后再试";
}
切面类
package com.qfedu.ratelimit.aspect;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.redisson.api.RRateLimiter;
import org.redisson.api.RateType;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Aspect
@Component
public class RateLimitAspect {@Autowiredprivate RedissonClient redissonClient;@Around("@annotation(rateLimit)")public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {String suffix = null;if (rateLimit.type() == RateLimitTypeEnum.TYPE_IP) {suffix = IpAddressUtil.getClientIpAddress();} else if (rateLimit.type() == RateLimitTypeEnum.TYPE_USER) {suffix = TokenUtils.getUidFromToken().toString();}String key = "rate:limit:" + suffix;RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);rateLimiter.trySetRate(RateType.OVERALL,rateLimit.rate(),rateLimit.interval(),rateLimit.unit());if (!rateLimiter.tryAcquire()) {throw new RuntimeException(rateLimit.message());}return joinPoint.proceed();}}
测试控制层方法
@RateLimit(type = RateLimitTypeEnum.TYPE_USER, rate = 10, interval = 60)@GetMapping("/test2")public String test2() {return "success";}
根据配置,执行到第11次时,报错
RRateLimiter使用的限流算法
RRateLimiter 使用的是令牌桶算法(Token Bucket Algorithm)
令牌桶算法的工作机制:
令牌生成:系统以固定速率向桶中添加令牌
令牌消耗:请求需要获取令牌才能被处理
桶容量:桶有最大容量,防止令牌无限累积
限流逻辑:当桶中有足够令牌时请求通过,否则被限流
令牌桶 vs 漏桶算法
特性 | 令牌桶算法 | 漏桶算法 |
---|---|---|
突发流量 | 允许一定程度的突发 | 严格限制,平滑输出 |
灵活性 | 更灵活,可应对突发 | 更严格,输出恒定 |
实现复杂度 | 相对简单 | 相对复杂 |
适用场景 | API限流、流量控制 | 网络流量整形 |
漏桶算法(Leaky Bucket Algorithm)核心概念:
漏桶算法是一种流量整形算法,它以恒定的速率处理请求,无论输入流量多么不规则。
工作机制:
请求到达,请求以任意速率进入桶中,相当于水流入,水量增加
恒定处理,以固定速率从桶中取出请求处理,相当于水流出,水以恒定速率从底部漏出
溢出拒绝:当桶满时,新请求被丢弃或拒绝,类似水满则溢