Annotation + redis 電流制限用、ええ、それは本当に香ばしいです

序文

プロジェクトで作業する際に外部インターフェースを提供する. 頻繁な呼び出しによるサーバーのクラッシュを防ぐために、アノテーション + redis を使用して電流制限機能を作成しました. 使用後は、本当に香ばしいとしか言​​えません.

機能要件

  • IP アドレスを要求することでフローを制限できます。たとえば、インターフェイスは 1 分間に 10 回しか呼び出せません。
  • トラフィックは、リクエスト パラメータによって制限できます。たとえば、user=zhangsan1 分あたり 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 {
    ....
}
复制代码

おすすめ

転載: juejin.im/post/7222179242956259383