Springboot 实现接口限流之分布式

上文中介绍类单实例下如何在业务接口层做限流,本文主要介绍分布式场景下限流的方案,以及什么样的场景下需要在业务层级加限流而不是接入层;

并且结合开源的ratelimiter-spring-boot-starter为例。

1. 准备知识点

我们需要分布式限流接入层限流来进行全局限流

  1. redis + lua 实现lua脚本

  2. 使用Nginx + Lua实现的lua脚本

  3. 使用OpenResty开源的限流方案

  4. 限流框架,比如Sentinel实现降级限流熔断

2. redis + lua封装

redis+lua时代码层实现比较常见的方案,以作者kailing的开源ratelimiter-spring-boot-starter为例,学习思路+代码封装+starter封装

2.1 使用场景

为什么有些分布式场景下,还会在代码层进行控制限流?

基于 redis 的偏业务应用的分布式限流组件,目前支持时间窗口、令牌桶两种限流算法。使得项目拥有分布式限流能力变得很简单。限流的场景有很多,常说的限流一般指网关限流,控制好洪峰流量,以免打垮后方应用。这里突出偏业务应用的分布式限流
的原因,是因为区别于网关限流,业务侧限流可以轻松根据业务性质做到细粒度的流量控制。比如如下场景:

  • 案例一:

    一个公开的openapi接口,给接入方派发一个appId,此时,如果需要根据各接入方的appId限流,网关限流就不好做来,只能在业务侧实现

  • 案例二:

    公司内部的短信接口,内部对接来多个第三方短信通道,每个短信通道对流量的控制都不相同,假设有的第三方根据手机号和短信模版组合限流,网关限流就不好做来

接下来看作者kailing时如何封装实现ratelimiter-spring-boot-starter

2.2 源代码的要点

  1. 引入包
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    ext {
    redisson_Version = '3.15.1'
    }

    dependencies {
    compile "org.redisson:redisson:${redisson_Version}"
    compile 'org.springframework.boot:spring-boot-starter-aop'
    compileOnly 'org.springframework.boot:spring-boot-starter-web'

    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springdoc:springdoc-openapi-ui:1.5.2'
    }
  1. RateLimit注解

    作者考虑了时间表达式,限流后的自定义回退后的拒绝逻辑, 用户自定义Key(PS:这里其实可以加一些默认的Key生成策略,比如按照方法策略, 按照方法&IP 策略, 按照自定义策略等,默认为按照方法)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    package com.taptap.ratelimiter.annotation;

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;

    /**
    * @author kl (http://kailing.pub)
    * @since 2021/3/16
    */
    @Target(value = {ElementType.METHOD})
    @Retention(value = RetentionPolicy.RUNTIME)
    public @interface RateLimit {

    /**
    * 时间窗口流量数量
    * @return rate
    */
    long rate();
    /**
    * 时间窗口流量数量表达式
    * @return rateExpression
    */
    String rateExpression() default "";

    /**
    * 时间窗口,最小单位秒,如 2s,2h , 2d
    * @return rateInterval
    */
    String rateInterval();

    /**
    * 获取key
    * @return keys
    */
    String [] keys() default {};

    /**
    * 限流后的自定义回退后的拒绝逻辑
    * @return fallback
    */
    String fallbackFunction() default "";

    /**
    * 自定义业务 key 的 Function
    * @return key
    */
    String customKeyFunction() default "";

    }
  2. AOP 拦截

    around环绕方式, 通过定义RateLimiterService获取方法注解的信息,存放在为RateLimiterInfo如果还定义了回调方法,被限流后还会执行回调方法,回调方法也在RateLimiterService中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    package com.taptap.ratelimiter.core;

    import com.taptap.ratelimiter.annotation.RateLimit;
    import com.taptap.ratelimiter.exception.RateLimitException;
    import com.taptap.ratelimiter.model.LuaScript;
    import com.taptap.ratelimiter.model.RateLimiterInfo;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.redisson.api.RScript;
    import org.redisson.api.RedissonClient;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.core.annotation.Order;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;

    import java.util.ArrayList;
    import java.util.List;

    /**
    * Created by kl on 2017/12/29.
    * Content : 切面拦截处理器
    */
    @Aspect
    @Component
    @Order(0)
    public class RateLimitAspectHandler {

    private static final Logger logger = LoggerFactory.getLogger(RateLimitAspectHandler.class);

    private final RateLimiterService rateLimiterService;
    private final RScript rScript;

    public RateLimitAspectHandler(RedissonClient client, RateLimiterService lockInfoProvider) {
    this.rateLimiterService = lockInfoProvider;
    this.rScript = client.getScript();
    }

    @Around(value = "@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
    RateLimiterInfo limiterInfo = rateLimiterService.getRateLimiterInfo(joinPoint, rateLimit);

    List<Object> keys = new ArrayList<>();
    keys.add(limiterInfo.getKey());
    keys.add(limiterInfo.getRate());
    keys.add(limiterInfo.getRateInterval());
    List<Long> results = rScript.eval(RScript.Mode.READ_WRITE, LuaScript.getRateLimiterScript(), RScript.ReturnType.MULTI, keys);
    boolean allowed = results.get(0) == 0L;
    if (!allowed) {
    logger.info("Trigger current limiting,key:{}", limiterInfo.getKey());
    if (StringUtils.hasLength(rateLimit.fallbackFunction())) {
    return rateLimiterService.executeFunction(rateLimit.fallbackFunction(), joinPoint);
    }
    long ttl = results.get(1);
    throw new RateLimitException("Too Many Requests", ttl);
    }
    return joinPoint.proceed();
    }
    }

    这里LuaScript加载定义的lua脚本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    package com.taptap.ratelimiter.model;

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.util.StreamUtils;

    import java.io.IOException;
    import java.io.InputStream;
    import java.nio.charset.StandardCharsets;

    /**
    * @author kl (http://kailing.pub)
    * @since 2021/3/18
    */
    public final class LuaScript {

    private LuaScript(){}
    private static final Logger log = LoggerFactory.getLogger(LuaScript.class);
    private static final String RATE_LIMITER_FILE_PATH = "META-INF/ratelimiter-spring-boot-starter-rateLimit.lua";
    private static String rateLimiterScript;

    static {
    InputStream in = Thread.currentThread().getContextClassLoader()
    .getResourceAsStream(RATE_LIMITER_FILE_PATH);
    try {
    rateLimiterScript = StreamUtils.copyToString(in, StandardCharsets.UTF_8);
    } catch (IOException e) {
    log.error("ratelimiter-spring-boot-starter Initialization failure",e);
    }
    }

    public static String getRateLimiterScript() {
    return rateLimiterScript;
    }
    }

    lua脚本放在META-INF/ratelimiter-spring-boot-starter-rateLimit.lua, 如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    --
    -- Created by IntelliJ IDEA.
    -- User: kl
    -- Date: 2021/3/18
    -- Time: 11:17 上午
    -- To change this template use File | Settings | File Templates.
    local rateLimitKey = KEYS[1];
    local rate = tonumber(KEYS[2]);
    local rateInterval = tonumber(KEYS[3]);
    local limitResult = 0;
    local ttlResult = 0;
    local currValue = redis.call('incr', rateLimitKey);
    if (currValue == 1) then
    redis.call('expire', rateLimitKey, rateInterval);
    limitResult = 0;
    else
    if (currValue > rate) then
    limitResult = 1;
    ttlResult = redis.call('ttl', rateLimitKey);
    end
    end
    return { limitResult, ttlResult }
  3. starter自动装配

    RateLimiterAutoConfiguration + RateLimiterProperties + spring.factories

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    package com.taptap.ratelimiter.configuration;

    import com.taptap.ratelimiter.core.BizKeyProvider;
    import com.taptap.ratelimiter.core.RateLimitAspectHandler;
    import com.taptap.ratelimiter.core.RateLimiterService;
    import com.taptap.ratelimiter.web.RateLimitExceptionHandler;
    import io.netty.channel.nio.NioEventLoopGroup;
    import org.redisson.Redisson;
    import org.redisson.api.RedissonClient;
    import org.redisson.codec.JsonJacksonCodec;
    import org.redisson.config.Config;
    import org.springframework.boot.autoconfigure.AutoConfigureAfter;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Import;

    /**
    * @author kl (http://kailing.pub)
    * @since 2021/3/16
    */
    @Configuration
    @ConditionalOnProperty(prefix = RateLimiterProperties.PREFIX, name = "enabled", havingValue = "true")
    @AutoConfigureAfter(RedisAutoConfiguration.class)
    @EnableConfigurationProperties(RateLimiterProperties.class)
    @Import({RateLimitAspectHandler.class, RateLimitExceptionHandler.class})
    public class RateLimiterAutoConfiguration {

    private final RateLimiterProperties limiterProperties;

    public RateLimiterAutoConfiguration(RateLimiterProperties limiterProperties) {
    this.limiterProperties = limiterProperties;
    }

    @Bean(destroyMethod = "shutdown")
    @ConditionalOnMissingBean
    RedissonClient redisson() {
    Config config = new Config();
    if (limiterProperties.getRedisClusterServer() != null) {
    config.useClusterServers().setPassword(limiterProperties.getRedisPassword())
    .addNodeAddress(limiterProperties.getRedisClusterServer().getNodeAddresses());
    } else {
    config.useSingleServer().setAddress(limiterProperties.getRedisAddress())
    .setDatabase(limiterProperties.getRedisDatabase())
    .setPassword(limiterProperties.getRedisPassword());
    }
    config.setCodec(new JsonJacksonCodec());
    config.setEventLoopGroup(new NioEventLoopGroup());
    return Redisson.create(config);
    }

    @Bean
    public RateLimiterService rateLimiterInfoProvider() {
    return new RateLimiterService();
    }

    @Bean
    public BizKeyProvider bizKeyProvider() {
    return new BizKeyProvider();
    }
    }

3. 示例源码

https://gitee.com/kailing/ratelimiter-spring-boot-starter