上文中介绍类单实例下如何在业务接口层做限流,本文主要介绍分布式场景下限流的方案,以及什么样的场景下需要在业务层级加限流而不是接入层;
1. 准备知识点
我们需要分布式限流和接入层限流来进行全局限流
redis + lua 实现lua脚本
使用Nginx + Lua实现的lua脚本
使用OpenResty开源的限流方案
限流框架,比如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
2
3
4
5
6
7
8
9
10
11
12
13
14ext {
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'
}
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
51package 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
*/
public 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 "";
}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
60package 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 : 切面拦截处理器
*/
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();
}
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
35package 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 }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
64package 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
*/
public class RateLimiterAutoConfiguration {
private final RateLimiterProperties limiterProperties;
public RateLimiterAutoConfiguration(RateLimiterProperties limiterProperties) {
this.limiterProperties = limiterProperties;
}
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);
}
public RateLimiterService rateLimiterInfoProvider() {
return new RateLimiterService();
}
public BizKeyProvider bizKeyProvider() {
return new BizKeyProvider();
}
}