序文
プロジェクトで作業する際に外部インターフェースを提供する. 頻繁な呼び出しによるサーバーのクラッシュを防ぐために、アノテーション + redis を使用して電流制限機能を作成しました. 使用後は、本当に香ばしいとしか言えません.
機能要件
- IP アドレスを要求することでフローを制限できます。たとえば、インターフェイスは 1 分間に 10 回しか呼び出せません。
- トラフィックは、リクエスト パラメータによって制限できます。たとえば、
user=zhangsan
1 分あたり 20 回の呼び出しのみに制限する場合などです。 - 基本的な注釈が満たされていない場合は、現在の制限戦略をカスタマイズして、戦略をリアルタイムで変更することもできます。
- より高度な機能: 現在の制限コンソール、現在の制限状況と現在の制限戦略をリアルタイムで表示できます (まだ実装されていません)。
機能実現
1. 注釈を定義する
IP 電流制限に関する注意事項
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IpCurrentLimit {
String[] value() default "*";
int limit() default 0;
int second() default 0;
String returnValue() default "请求频繁,请稍后访问!";
Class<? extends CustomReturnInvocation> CustomReturnObject() default CustomReturnInvocation.class;
}
复制代码
String[] value(): 制限する必要がある IP アドレス (例: 127.0.0.1)。値は配列で、複数の IP アドレスがサポートされていることがわかります。
int limit(): 制限する数。second() の値と組み合わせて使用すると、どのくらいの時間内に何回制限されるかを示します。
int second(): 時間、単位は秒です。limit() の値と組み合わせて使用すると、一定期間内に制限される回数を示します。
returnValue(): 戻り値リマインダー。たとえば、現在の制限のリマインダー: 頻繁にリクエストされる場合は、後でアクセスしてください!
CustomReturnObject(): オブジェクトを返す場合など、戻り値をカスタマイズするには、この構成を使用する必要があります。
パラメータ電流制限注釈
パラメータの電流制限と ip 注釈の電流制限は似ていますが、唯一の違いは電流制限のパラメータです。分離の理由は、分離使用のロジックがより明確になるためであり、2 つ目は、複数構成の電流制限をサポートするためです。たとえば、IP 分割とパラメータ電流制限の両方を 1 つの方法に適用できます。ソリューションは「回路」を採用しています。ポリシーが満たされると 、現在の制限がすぐにトリガーされます。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ParameterCurrentLimit {
String[] value() default "";
int limit() default 0;
int second() default 0;
String returnValue() default "请求频繁,请稍后访问!";
Class<? extends CustomReturnInvocation> CustomReturnObject() default CustomReturnInvocation.class;
}
复制代码
設定パラメータはすべてほぼ同じです。
String[] value(): 制限されたパラメーター設定。* でマークされた構成とパラメーター値の間の通常の一致をサポートします。例えば:
name=* 、 name パラメータの値は現在の制限によって制限されます。
user=n({az}+)e の場合、パラメーター user の値が正規表現の値を満たす場合、フローは制限されます。例: ユーザー = 名前、ユーザー = nsime。
user=zhangsan , 当参数user的值等于 zhangsan 时,将被限流,其他的都不会被限流
自定义限流策略
自定义限流可以实现更加灵活的限流的策略和实时更新限流策略,实现策略的动态增加和删除。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomCurrentLimit {
Class<? extends CustomCurrentLimitInjection> value() default CustomCurrentLimitInjection.class;
Class<? extends CustomReturnInvocation> CustomReturnObject() default CustomReturnInvocation.class;
}
复制代码
public class CustomCurrentLimitElement implements Serializable {
private final int limit;
private final int second;
private final String[] limitObject;
private final CurrentLimitStrategyTypeEnum getStrategyType;
private final String returnValue;
}
复制代码
CustomCurrentLimit :
Class<? extends CustomCurrentLimitInjection> value() 可以传入自定义限量策略组。
Class<? extends CustomReturnInvocation> CustomReturnObject() 自定义返回值。
CustomCurrentLimitElement : 该类的属性和注解配置的含义差不多。只是需要用CurrentLimitStrategyTypeEnum 指定限流策略的类型。
2.请求拦截
这里我是用的AOP的方式,在项目中我实现了一个AOP的统一拦截框架,用于实现一个请求的统一处理(当然也可以使用 HandlerInterceptor 拦截请求实现改功能)。两者主要逻辑基本一致。
IpCurrentLimit ipCurrentLimit = context.getAnnotationOnMethod(IpCurrentLimit.class);
ParameterCurrentLimit parameterAnnotation = context.getAnnotationOnMethod(ParameterCurrentLimit.class);
CustomCurrentLimit customCurrentLimit = context.getAnnotationOnMethod(CustomCurrentLimit.class);
String requestURI = context.getRequest().getRequestURI();
boolean isLimit = false;
try {
if (ipCurrentLimit != null) {
CurrentLimitExecute.registerStrategy(requestURI, ipCurrentLimit);
invocation = ipCurrentLimit.CustomReturnObject();
isLimit = true;
}
if (parameterAnnotation != null) {
CurrentLimitExecute.registerStrategy(requestURI, parameterAnnotation);
invocation = parameterAnnotation.CustomReturnObject();
isLimit = true;
}
if (customCurrentLimit != null) {
Class<? extends CustomCurrentLimitInjection> value = customCurrentLimit.value();
CustomCurrentLimitElement[] element = value.newInstance().registerStrategy(requestURI);
if (element != null) {
invocation = customCurrentLimit.CustomReturnObject();
CurrentLimitExecute.registerStrategy(requestURI, element);
isLimit = true;
}
}
if (isLimit) {
try {
CurrentLimitExecute.accessible(context.getRequest());
} catch (CurrentLimitException e) {
Object returnValue = e.getStrategy().getMessage();
if (!invocation.getName().equals(CustomCurrentLimitInjection.class.getName())) {
returnValue = invocation.newInstance().invoke(requestURI);
}
throw new DplAspectException("", returnValue);
}
}
} catch (InstantiationException | IllegalAccessException e) {
throw new DplAspectException("", "服务器限流策略执行错误");
}
复制代码
该方法的作用是:在AOP中对请求进行限流判断和处理。具体的实现方式是,首先获取方法上标注的IpCurrentLimit、ParameterCurrentLimit和CustomCurrentLimit注解,这些注解分别表示对IP、请求参数和自定义限流策略进行限制。然后根据这些注解和请求的URI注册限流策略,在访问方法前对请求进行访问控制,如果超过了限制就会抛出CurrentLimitException异常。如果抛出了异常,就会根据不同情况返回不同的信息。
其中CurrentLimitExecute是一个限流策略注册和访问控制的工具类,CustomCurrentLimitInjection是一个抽象类,表示自定义的限流策略实现,它需要实现invoke方法来返回限流时的响应信息。invocation是一个CustomCurrentLimitInjection对象,表示当前使用的自定义限流策略。如果当前限流异常是由自定义限流策略触发的,就会调用当前限流策略的invoke方法来返回响应信息。
3.限流解析
首先我定了一个 private static final Map<String, List<CurrentLimitStrategy>> map = new HashMap<>();
定一个Map用于缓存请求方法和该方法的限流策略组的绑定关系。核心执行逻辑如下:
String requestURI = request.getRequestURI();
String[] parameterKey = strategy.limitParameterKey(requestURI);
if (parameterKey != null) {
CurrentLimitStrategy[] strategyArr = strategy.getParameterStrategy(requestURI);
List<String> limitValue = new ArrayList<>();
for (String key : parameterKey) {
limitValue.add("pm:" + key + "=" + request.getParameter(key));
}
getCurrentLimitServices().accessible(requestURI, limitValue.toArray(new String[0]), strategyArr);
}
if (strategy.limitIp(requestURI)) {
CurrentLimitStrategy[] strategyArr = strategy.getIpStrategy(requestURI);
getCurrentLimitServices().accessible(requestURI, new String[]{"ip:" + getIpAdder(request)}, strategyArr);
}
复制代码
该方法大概是:从缓存中获取保存的参数限流策略和Ip限流策略,然后获取调用 getCurrentLimitServices().accessible(requestURI, limitValue.toArray(new String[0]), strategyArr);
进行限流判断。
parameterKey:是一种自定义拼装的Key,将参数值进行拼装:
limitValue.add("pm:" + key + "=" + request.getParameter(key));
4.核心判断逻辑
for (String key : limitKey) {
String finalKey = key;
// 1. 多种策略时,当满足一种策略后,就不再像后匹配。所以在多个策略的时候,需要对策略排序的功能。 采用优先匹配策略
List<CurrentLimitStrategy> currentLimitStrategies = Arrays.stream(strategy).
filter(cell -> cell.match(finalKey)).collect(Collectors.toList());
for (CurrentLimitStrategy currentLimitStrategy : currentLimitStrategies) {
// 2
if (currentLimitStrategy.limit() == -1 || currentLimitStrategy.second() == -1) {
continue;
}
// 3
if (currentLimitStrategy.limit() == 0 || currentLimitStrategy.second() == 0) {
throw new CurrentLimitException("超出请求限制", currentLimitStrategy);
}
key = url + key;
// 4
Integer maxLimit = redisTemplate().opsForValue().get(key);
// 5
if (maxLimit == null) {
redisTemplate.opsForValue().set(key, 1, currentLimitStrategy.second(), TimeUnit.SECONDS);
} else if (maxLimit < currentLimitStrategy.limit()) { // 6
redisTemplate.opsForValue().set(key, maxLimit + 1, currentLimitStrategy.second(), TimeUnit.SECONDS);
} else { // 7
log.info("参数{}执行的受限制策略:{}", String.join(",", limitKey), currentLimitStrategy);
throw new CurrentLimitException("超出请求限制", currentLimitStrategy);
}
}
}
复制代码
方法解析:
1.循环找出满足条件的策略组。然后循环匹配,一旦满足条件就抛出CurrentLimitException异常。
2.判断策略的默认值。如果limit=-1或者second=-1直接跳过。
3.判断策略的默认值。如果limit=0或者second=0 则直接抛出CurrentLimitException异常。
4.从redis中根据当前值获取缓存值。然后判断当前redis存储的请求数值与限流策略中的limit的大小。
5.如果maxLimit为0,则缓存改请求。
6.当maxLimit < currentLimitStrategy.limit()时,则缓存改请求。
7.当超出限制,则抛出异常。
tips:
1.缓存时我们使用了redis的缓存时间当作被限流的请求的时间。
2.CurrentLimitStrategy.match()方法根据不同的限流策略实现。
5.注解标记
@CustomCurrentLimit(value = DbCustomCurrentLimitInjection.class, CustomReturnObject =
DbCustomReturnInvocation.class)
public Msg getUser(String id, HttpServletResponse response, HttpServletRequest request) throws FileNotFoundException {
....
}
复制代码