Skip to content

Commit b065ad3

Browse files
committed
添加Redis分布式限流方式实现
1 parent dc0677b commit b065ad3

File tree

6 files changed

+304
-6
lines changed

6 files changed

+304
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright [2019] [恒宇少年 - 于起宇]
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package org.minbox.framework.api.boot.plugin.rate.limiter;
19+
20+
/**
21+
* ApiBoot RateLimiter
22+
*
23+
* @author:恒宇少年 - 于起宇
24+
* <p>
25+
* DateTime:2019-05-05 17:12
26+
* Blog:http://blog.yuqiyu.com
27+
* WebSite:http://www.jianshu.com/u/092df3f77bca
28+
* Gitee:https://gitee.com/hengboy
29+
* GitHub:https://github.com/hengboy
30+
*/
31+
public interface ApiBootRateLimiter {
32+
/**
33+
* Attempt to obtain a request current limit token
34+
*
35+
* @param QPS queries per second
36+
* @param requestUri request uri
37+
* @return true : allow access to
38+
*/
39+
boolean tryAcquire(Double QPS, String requestUri);
40+
41+
class Response {
42+
private final boolean allowed;
43+
private final long tokensRemaining;
44+
45+
public Response(boolean allowed, long tokensRemaining) {
46+
this.allowed = allowed;
47+
this.tokensRemaining = tokensRemaining;
48+
}
49+
50+
public boolean isAllowed() {
51+
return allowed;
52+
}
53+
54+
@Override
55+
public String toString() {
56+
return "Response{" +
57+
"allowed=" + allowed +
58+
", tokensRemaining=" + tokensRemaining +
59+
'}';
60+
}
61+
}
62+
}

api-boot-project/api-boot-plugins/api-boot-plugin-rate-limiter/src/main/java/org/minbox/framework/api/boot/plugin/rate/limiter/ApiBootRateLimiterConfiguration.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public ApiBootRateLimiterConfiguration(RateLimiterConfig rateLimiterConfig, Hand
5454
this.rateLimiterConfig = rateLimiterConfig;
5555
this.handlerInterceptor = handlerInterceptor;
5656
Assert.notNull(rateLimiterConfig, "RateLimiterConfig can not be null.");
57-
Assert.notNull(rateLimiterConfig, "HandlerInterceptor can not be null.");
57+
Assert.notNull(handlerInterceptor, "HandlerInterceptor can not be null.");
5858
}
5959

6060
@Override

api-boot-project/api-boot-plugins/api-boot-plugin-rate-limiter/src/main/java/org/minbox/framework/api/boot/plugin/rate/limiter/handler/ApiBootDefaultRateLimiterInterceptorHandler.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@
1717

1818
package org.minbox.framework.api.boot.plugin.rate.limiter.handler;
1919

20+
import org.minbox.framework.api.boot.plugin.rate.limiter.ApiBootRateLimiter;
2021
import org.minbox.framework.api.boot.plugin.rate.limiter.annotation.RateLimiter;
21-
import org.minbox.framework.api.boot.plugin.rate.limiter.context.ApiBootRateLimiterContext;
2222
import org.slf4j.Logger;
2323
import org.slf4j.LoggerFactory;
24+
import org.springframework.util.Assert;
25+
import org.springframework.util.ObjectUtils;
2426
import org.springframework.web.method.HandlerMethod;
2527
import org.springframework.web.servlet.HandlerInterceptor;
2628

@@ -43,6 +45,16 @@ public class ApiBootDefaultRateLimiterInterceptorHandler implements HandlerInter
4345
* logger instance
4446
*/
4547
static Logger logger = LoggerFactory.getLogger(ApiBootDefaultRateLimiterInterceptorHandler.class);
48+
/**
49+
* ApiBoot RateLimiter
50+
*/
51+
private ApiBootRateLimiter apiBootRateLimiter;
52+
53+
public ApiBootDefaultRateLimiterInterceptorHandler(ApiBootRateLimiter apiBootRateLimiter) {
54+
this.apiBootRateLimiter = apiBootRateLimiter;
55+
Assert.notNull(apiBootRateLimiter, "No ApiBootRateLimiter implementation class instance.");
56+
logger.info("ApiBootDefaultRateLimiterInterceptorHandler load complete.");
57+
}
4658

4759
/**
4860
* Executing traffic restrictions
@@ -59,12 +71,14 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons
5971
HandlerMethod handlerMethod = (HandlerMethod) handler;
6072
// get method declared RateLimiter
6173
RateLimiter rateLimiterAnnotation = handlerMethod.getMethodAnnotation(RateLimiter.class);
62-
com.google.common.util.concurrent.RateLimiter rateLimiter = ApiBootRateLimiterContext.cacheRateLimiter(request.getRequestURI(), rateLimiterAnnotation.QPS());
63-
double acquire = rateLimiter.acquire();
64-
logger.debug("ApiBoot rate limiter acquire:{}", acquire);
74+
// if dont't set RateLimiter
75+
if (ObjectUtils.isEmpty(rateLimiterAnnotation)) {
76+
return true;
77+
}
78+
return apiBootRateLimiter.tryAcquire(rateLimiterAnnotation.QPS(), request.getRequestURI());
6579
} catch (Exception e) {
80+
logger.error("Current Limiting Request Encountered Exception.", e);
6681
return false;
6782
}
68-
return true;
6983
}
7084
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright [2019] [恒宇少年 - 于起宇]
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package org.minbox.framework.api.boot.plugin.rate.limiter.support;
19+
20+
import org.minbox.framework.api.boot.plugin.rate.limiter.ApiBootRateLimiter;
21+
22+
/**
23+
* ApiBoot RateLimiter Abstract Support
24+
*
25+
* @author:恒宇少年 - 于起宇
26+
* <p>
27+
* DateTime:2019-05-05 17:19
28+
* Blog:http://blog.yuqiyu.com
29+
* WebSite:http://www.jianshu.com/u/092df3f77bca
30+
* Gitee:https://gitee.com/hengboy
31+
* GitHub:https://github.com/hengboy
32+
*/
33+
public abstract class AbstractRateLimiter implements ApiBootRateLimiter {
34+
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright [2019] [恒宇少年 - 于起宇]
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package org.minbox.framework.api.boot.plugin.rate.limiter.support;
19+
20+
import org.minbox.framework.api.boot.plugin.rate.limiter.context.ApiBootRateLimiterContext;
21+
22+
/**
23+
* google guava rate limiter support
24+
*
25+
* @author:恒宇少年 - 于起宇
26+
* <p>
27+
* DateTime:2019-05-05 17:20
28+
* Blog:http://blog.yuqiyu.com
29+
* WebSite:http://www.jianshu.com/u/092df3f77bca
30+
* Gitee:https://gitee.com/hengboy
31+
* GitHub:https://github.com/hengboy
32+
*/
33+
public class GoogleGuavaRateLimiter extends AbstractRateLimiter {
34+
/**
35+
* google guava away
36+
*
37+
* @param QPS queries per second
38+
* @param requestUri request uri
39+
* @return true : allow access to
40+
*/
41+
@Override
42+
public boolean tryAcquire(Double QPS, String requestUri) {
43+
com.google.common.util.concurrent.RateLimiter rateLimiter = ApiBootRateLimiterContext.cacheRateLimiter(requestUri, QPS);
44+
return rateLimiter.tryAcquire();
45+
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright [2019] [恒宇少年 - 于起宇]
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
package org.minbox.framework.api.boot.plugin.rate.limiter.support;
19+
20+
import org.slf4j.Logger;
21+
import org.slf4j.LoggerFactory;
22+
import org.springframework.core.io.ClassPathResource;
23+
import org.springframework.data.redis.core.RedisTemplate;
24+
import org.springframework.data.redis.core.script.DefaultRedisScript;
25+
import org.springframework.data.redis.core.script.RedisScript;
26+
import org.springframework.data.redis.serializer.RedisSerializer;
27+
import org.springframework.data.redis.serializer.StringRedisSerializer;
28+
import org.springframework.scripting.support.ResourceScriptSource;
29+
import org.springframework.util.Assert;
30+
31+
import java.time.Instant;
32+
import java.util.Arrays;
33+
import java.util.List;
34+
35+
/**
36+
* redis lua rate limiter support
37+
*
38+
* @author:恒宇少年 - 于起宇
39+
* <p>
40+
* DateTime:2019-05-05 17:21
41+
* Blog:http://blog.yuqiyu.com
42+
* WebSite:http://www.jianshu.com/u/092df3f77bca
43+
* Gitee:https://gitee.com/hengboy
44+
* GitHub:https://github.com/hengboy
45+
*/
46+
public class RedisLuaRateLimiter extends AbstractRateLimiter {
47+
/**
48+
* logger instance
49+
*/
50+
static Logger logger = LoggerFactory.getLogger(RedisLuaRateLimiter.class);
51+
52+
/**
53+
* Redis Script file name.
54+
*/
55+
private static final String QPS_LUA_PATH = "META-INF/scripts/qps-rate-limiter.lua";
56+
/**
57+
* redis template
58+
*/
59+
private RedisTemplate redisTemplate;
60+
/**
61+
* Redis Script.
62+
*/
63+
private RedisScript<List<Long>> redisScript;
64+
65+
public RedisLuaRateLimiter(RedisTemplate redisTemplate) {
66+
this.redisTemplate = redisTemplate;
67+
this.redisScript = getRedisScript();
68+
69+
Assert.notNull(redisTemplate, "No RedisTemplate implementation class was found.");
70+
Assert.notNull(redisScript, "Unable to load Lua script.");
71+
}
72+
73+
/**
74+
* redis lua away
75+
* Processing traffic restrictions using LUA scripts
76+
* Processing with Spring Cloud Gateway official script
77+
*
78+
* @param QPS queries per second
79+
* @param requestUri request uri
80+
* @return true : allow access to
81+
*/
82+
@Override
83+
public boolean tryAcquire(Double QPS, String requestUri) {
84+
try {
85+
// get keys
86+
List<String> keys = getKeys(requestUri);
87+
88+
// Parameters and serialization of return values
89+
RedisSerializer<String> serializer = new StringRedisSerializer();
90+
91+
// execute lua script
92+
List<Long> tokenResult = (List<Long>) this.redisTemplate.execute(this.redisScript, serializer, serializer, keys, String.valueOf(QPS.intValue()), String.valueOf(QPS.intValue()), Instant.now().getEpochSecond() + "", "1");
93+
94+
// Index 1 value is the number of remaining requestable tokens
95+
Long newTokens = tokenResult.get(1);
96+
if (logger.isDebugEnabled()) {
97+
logger.debug("Number of remaining tokens for this request is {}", newTokens);
98+
}
99+
return newTokens > 0;
100+
} catch (Exception e) {
101+
/*
102+
* We don't want a hard dependency on Redis to allow traffic. Make sure to set
103+
* an alert so you know if this is happening too much. Stripe's observed
104+
* failure rate is 0.01%.
105+
*/
106+
logger.error("Error determining if user allowed from redis", e);
107+
}
108+
return true;
109+
}
110+
111+
/**
112+
* get Redis Script
113+
*
114+
* @return RedisScript
115+
*/
116+
RedisScript<List<Long>> getRedisScript() {
117+
DefaultRedisScript redisScript = new DefaultRedisScript<>();
118+
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(QPS_LUA_PATH)));
119+
redisScript.setResultType(List.class);
120+
return redisScript;
121+
}
122+
123+
/**
124+
* get Keys
125+
*
126+
* @param id resource key(request uri)
127+
* @return
128+
*/
129+
static List<String> getKeys(String id) {
130+
// use `{}` around keys to use Redis Key hash tags
131+
// this allows for using redis cluster
132+
133+
// Make a unique key per user.
134+
String prefix = "qps_rate_limiter.{" + id;
135+
136+
// You need two Redis keys for Token Bucket.
137+
String tokenKey = prefix + "}.tokens";
138+
String timestampKey = prefix + "}.timestamp";
139+
return Arrays.asList(tokenKey, timestampKey);
140+
}
141+
}

0 commit comments

Comments
 (0)