使用Spring AOP和番石榴速率限制器的节气门方法

外部服务或API可能有使用限制,或者它们不能失败就无法处理大量请求。 这篇文章解释了如何创建一个基于Spring Framework的方面,该方面可以用来限制使用Guava速率限制器的任何建议方法调用。 以下实现需要Java 8,Spring AOP和Guava。

让我们从注释开始,该注释用于建议任何启用Spring AOP的方法调用。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
 
    /**
     * @return rate limit in queries per second
     */
    int value();
 
    /**
     * @return rate limiter identifier (optional)
     */
    String key() default "";
 
}

注释定义了两件事:每秒查询(或许可)中的速率限制,以及标识速率限制器的可选键。 如果密钥相等,则多种方法可以使用相同的速率限制器。 例如,当使用来自不同方法的不同参数调用API时,每秒所需的总查询次数将不会超过。

接下来是实际的节流方面,它是作为Spring Framework组件实现的。 无论有没有Spring框架,在任何情况下都可以使用方面。

@Aspect
@Component
public class RateLimiterAspect {
 
    public interface KeyFactory {
        String createKey(JoinPoint jp, RateLimit limit);
    }
 
    private static final Logger LOGGER = LoggerFactory.getLogger(RateLimiterAspect.class);
 
    private static final KeyFactory DEFAULT_KEY_FACTORY = (jp, limit) -> JoinPointToStringHelper.toString(jp);
    
    private final ConcurrentHashMap<String, RateLimiter> limiters;
    private final KeyFactory keyFactory;
 
    @Autowired
    public RateLimiterAspect(Optional<KeyFactory> keyFactory) {
        this.limiters = new ConcurrentHashMap<>();
        this.keyFactory = keyFactory.orElse(DEFAULT_KEY_FACTORY);
    }
 
    @Before("@annotation(limit)")
    public void rateLimit(JoinPoint jp, RateLimit limit) {
        String key = createKey(jp, limit);
        RateLimiter limiter = limiters.computeIfAbsent(key, createLimiter(limit));
        double delay = limiter.acquire();
        LOGGER.debug("Acquired rate limit permission ({} qps) for {} in {} seconds", limiter.getRate(), key, delay);
    }
 
    private Function<String, RateLimiter> createLimiter(RateLimit limit) {
        return name -> RateLimiter.create(limit.value());
    }
 
    private String createKey(JoinPoint jp, RateLimit limit) {
        return Optional.ofNullable(Strings.emptyToNull(limit.key()))
                .orElseGet(() -> keyFactory.createKey(jp, limit));
    }
}

该类定义了密钥工厂的附加接口和默认实现,如果注释未为速率限制器提供显式密钥,则使用该默认工厂。 密钥工厂可以使用连接点(基本上是方法调用)和提供的注释为速率限制器创建合适的密钥。 该方面还使用并发哈希图来存储速率限制器实例。 方面被定义为单例,但是可以从多个线程调用rateLimit方法,因此并发哈希图确保我们为每个唯一键仅分配单个速率限制器。 方面的构造器注入利用了Spring Framework的可选注入支持。 如果在上下文中未定义KeyFactory bean,则使用默认的密钥工厂。

该类使用@Aspect和@Component进行注释,以便Spring理解已定义的方面并启用@Before建议。 @Before通知仅包含一个切入点,该切入点需要RateLimit批注并将其绑定到方法的limit参数。 节流的实现非常简单。 首先,为速率限制器创建一个密钥。 然后,使用密钥查找或创建限制器,最后获取限制器以获取许可。

速率限制器密钥创建中有一个小陷阱。 注释定义的键将转换为可选键,但由于性能原因,不能使用可选键的orElse方法。 可选的orElse方法采用一个在任何情况下(无论是否存在)可选的值。 另一方面,另一种方法或orElseGet使用了供应商,该供应商仅当不存在可选值时才允许懒惰地评估值。 密钥工厂的createKey可能是一项昂贵的操作,因此使用供应商版本。

并发哈希图包含一个方便的方法computeIfAbsent ,该方法自动基于键和已定义的函数查找或创建一个值。 这允许对映射值进行简单明了的延迟初始化。 速率限制器是按需创建的,并且保证每个唯一的限制器密钥只有一个实例。

默认的密钥工厂实现使用JoinPointToStringHelper中的帮助程序方法将连接点转换为文本表示形式。

public class JoinPointToStringHelper {
 
    public static String toString(JoinPoint jp) {
        StringBuilder sb = new StringBuilder();
        appendType(sb, getType(jp));
        Signature signature = jp.getSignature();
        if (signature instanceof MethodSignature) {
            MethodSignature ms = (MethodSignature) signature;
            sb.append("#");
            sb.append(ms.getMethod().getName());
            sb.append("(");
            appendTypes(sb, ms.getMethod().getParameterTypes());
            sb.append(")");
        }
        return sb.toString();
    }
 
    private static Class<?> getType(JoinPoint jp) {
        return Optional.ofNullable(jp.getSourceLocation())
                .map(SourceLocation::getWithinType)
                .orElse(jp.getSignature().getDeclaringType());
    }
 
    private static void appendTypes(StringBuilder sb, Class<?>[] types) {
        for (int size = types.length, i = 0; i < size; i++) {
            appendType(sb, types[i]);
            if (i < size - 1) {
                sb.append(",");
            }
        }
    }
 
    private static void appendType(StringBuilder sb, Class<?> type) {
        if (type.isArray()) {
            appendType(sb, type.getComponentType());
            sb.append("[]");
        } else {
            sb.append(type.getName());
        }
    }
}

最后,只需添加@RateLimit批注,即可将限制应用于任何启用Spring的方法。

@Service
public class MyService {
 
    ...
 
    @RateLimit(5)
    public String callExternalApi() {
        return restTemplate.getForEntity(url, String.class).getBody();
    }
 
}

有人可能想知道这种解决方案能否很好地扩展? 不,实际上不是。 Guava的速率限制器会阻止当前线程,因此,如果对受限制的服务进行异步调用,则大量线程将被阻止,并可能导致空闲线程耗尽。 如果在多个应用程序或JVM实例中复制服务,则会引起另一个问题。 没有限制器速率的全局同步。 对于驻留在单个JVM中的单个应用程序以及节流方法的适当负载,此实现效果很好。

进一步阅读:

翻译自: https://www.javacodegeeks.com/2015/07/throttle-methods-with-spring-aop-and-guava-rate-limiter.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值