Skip to content

Commit 57100fe

Browse files
committed
✨ spring-boot-demo-ratelimit-redis 分布式限流完成
1 parent d6e10e4 commit 57100fe

File tree

3 files changed

+296
-0
lines changed

3 files changed

+296
-0
lines changed
+296
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
# spring-boot-demo-ratelimit-redis
2+
3+
> 此 demo 主要演示了 Spring Boot 项目如何通过 AOP 结合 Redis 实现分布式限流,旨在保护 API 被恶意频繁访问的问题,是 `spring-boot-demo-ratelimit-guava` 的升级版。
4+
5+
## 1. 主要代码
6+
7+
### 1.1. pom.xml
8+
9+
```xml
10+
<?xml version="1.0" encoding="UTF-8"?>
11+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
12+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
13+
<modelVersion>4.0.0</modelVersion>
14+
15+
<artifactId>spring-boot-demo-ratelimit-redis</artifactId>
16+
<version>1.0.0-SNAPSHOT</version>
17+
<packaging>jar</packaging>
18+
19+
<name>spring-boot-demo-ratelimit-redis</name>
20+
<description>Demo project for Spring Boot</description>
21+
22+
<parent>
23+
<groupId>com.xkcoding</groupId>
24+
<artifactId>spring-boot-demo</artifactId>
25+
<version>1.0.0-SNAPSHOT</version>
26+
</parent>
27+
28+
<properties>
29+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
30+
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
31+
<java.version>1.8</java.version>
32+
</properties>
33+
34+
<dependencies>
35+
<dependency>
36+
<groupId>org.springframework.boot</groupId>
37+
<artifactId>spring-boot-starter-web</artifactId>
38+
</dependency>
39+
40+
<dependency>
41+
<groupId>org.springframework.boot</groupId>
42+
<artifactId>spring-boot-starter-aop</artifactId>
43+
</dependency>
44+
45+
<dependency>
46+
<groupId>org.springframework.boot</groupId>
47+
<artifactId>spring-boot-starter-data-redis</artifactId>
48+
</dependency>
49+
50+
<!-- 对象池,使用redis时必须引入 -->
51+
<dependency>
52+
<groupId>org.apache.commons</groupId>
53+
<artifactId>commons-pool2</artifactId>
54+
</dependency>
55+
56+
<dependency>
57+
<groupId>cn.hutool</groupId>
58+
<artifactId>hutool-all</artifactId>
59+
</dependency>
60+
61+
<dependency>
62+
<groupId>org.springframework.boot</groupId>
63+
<artifactId>spring-boot-starter-test</artifactId>
64+
<scope>test</scope>
65+
</dependency>
66+
67+
<dependency>
68+
<groupId>org.projectlombok</groupId>
69+
<artifactId>lombok</artifactId>
70+
<optional>true</optional>
71+
</dependency>
72+
</dependencies>
73+
74+
<build>
75+
<finalName>spring-boot-demo-ratelimit-redis</finalName>
76+
<plugins>
77+
<plugin>
78+
<groupId>org.springframework.boot</groupId>
79+
<artifactId>spring-boot-maven-plugin</artifactId>
80+
</plugin>
81+
</plugins>
82+
</build>
83+
84+
</project>
85+
```
86+
87+
### 1.2. 限流注解
88+
89+
```java
90+
/**
91+
* <p>
92+
* 限流注解,添加了 {@link AliasFor} 必须通过 {@link AnnotationUtils} 获取,才会生效
93+
* </p>
94+
*
95+
* @author yangkai.shen
96+
* @date Created in 2019/9/30 10:31
97+
* @see AnnotationUtils
98+
*/
99+
@Target(ElementType.METHOD)
100+
@Retention(RetentionPolicy.RUNTIME)
101+
@Documented
102+
public @interface RateLimiter {
103+
long DEFAULT_REQUEST = 10;
104+
105+
/**
106+
* max 最大请求数
107+
*/
108+
@AliasFor("max") long value() default DEFAULT_REQUEST;
109+
110+
/**
111+
* max 最大请求数
112+
*/
113+
@AliasFor("value") long max() default DEFAULT_REQUEST;
114+
115+
/**
116+
* 限流key
117+
*/
118+
String key() default "";
119+
120+
/**
121+
* 超时时长,默认1分钟
122+
*/
123+
long timeout() default 1;
124+
125+
/**
126+
* 超时时间单位,默认 分钟
127+
*/
128+
TimeUnit timeUnit() default TimeUnit.MINUTES;
129+
}
130+
```
131+
132+
### 1.3. AOP处理限流
133+
134+
```java
135+
/**
136+
* <p>
137+
* 限流切面
138+
* </p>
139+
*
140+
* @author yangkai.shen
141+
* @date Created in 2019/9/30 10:30
142+
*/
143+
@Slf4j
144+
@Aspect
145+
@Component
146+
@RequiredArgsConstructor(onConstructor_ = @Autowired)
147+
public class RateLimiterAspect {
148+
private final static String SEPARATOR = ":";
149+
private final static String REDIS_LIMIT_KEY_PREFIX = "limit:";
150+
private final StringRedisTemplate stringRedisTemplate;
151+
private final RedisScript<Long> limitRedisScript;
152+
153+
@Pointcut("@annotation(com.xkcoding.ratelimit.redis.annotation.RateLimiter)")
154+
public void rateLimit() {
155+
156+
}
157+
158+
@Around("rateLimit()")
159+
public Object pointcut(ProceedingJoinPoint point) throws Throwable {
160+
MethodSignature signature = (MethodSignature) point.getSignature();
161+
Method method = signature.getMethod();
162+
// 通过 AnnotationUtils.findAnnotation 获取 RateLimiter 注解
163+
RateLimiter rateLimiter = AnnotationUtils.findAnnotation(method, RateLimiter.class);
164+
if (rateLimiter != null) {
165+
String key = rateLimiter.key();
166+
// 默认用类名+方法名做限流的 key 前缀
167+
if (StrUtil.isBlank(key)) {
168+
key = method.getDeclaringClass().getName()+StrUtil.DOT+method.getName();
169+
}
170+
// 最终限流的 key 为 前缀 + IP地址
171+
// TODO: 此时需要考虑局域网多用户访问的情况,因此 key 后续需要加上方法参数更加合理
172+
key = key + SEPARATOR + IpUtil.getIpAddr();
173+
174+
long max = rateLimiter.max();
175+
long timeout = rateLimiter.timeout();
176+
TimeUnit timeUnit = rateLimiter.timeUnit();
177+
boolean limited = shouldLimited(key, max, timeout, timeUnit);
178+
if (limited) {
179+
throw new RuntimeException("手速太快了,慢点儿吧~");
180+
}
181+
}
182+
183+
return point.proceed();
184+
}
185+
186+
private boolean shouldLimited(String key, long max, long timeout, TimeUnit timeUnit) {
187+
// 最终的 key 格式为:
188+
// limit:自定义key:IP
189+
// limit:类名.方法名:IP
190+
key = REDIS_LIMIT_KEY_PREFIX + key;
191+
// 统一使用单位毫秒
192+
long ttl = timeUnit.toMillis(timeout);
193+
// 当前时间毫秒数
194+
long now = Instant.now().toEpochMilli();
195+
long expired = now - ttl;
196+
// 注意这里必须转为 String,否则会报错 java.lang.Long cannot be cast to java.lang.String
197+
Long executeTimes = stringRedisTemplate.execute(limitRedisScript, Collections.singletonList(key), now + "", ttl + "", expired + "", max + "");
198+
if (executeTimes != null) {
199+
if (executeTimes == 0) {
200+
log.error("【{}】在单位时间 {} 毫秒内已达到访问上限,当前接口上限 {}", key, ttl, max);
201+
return true;
202+
} else {
203+
log.info("【{}】在单位时间 {} 毫秒内访问 {} 次", key, ttl, executeTimes);
204+
return false;
205+
}
206+
}
207+
return false;
208+
}
209+
}
210+
```
211+
212+
### 1.4. lua 脚本
213+
214+
```lua
215+
-- 下标从 1 开始
216+
local key = KEYS[1]
217+
local now = tonumber(ARGV[1])
218+
local ttl = tonumber(ARGV[2])
219+
local expired = tonumber(ARGV[3])
220+
-- 最大访问量
221+
local max = tonumber(ARGV[4])
222+
223+
-- 清除过期的数据
224+
-- 移除指定分数区间内的所有元素,expired 即已经过期的 score
225+
-- 根据当前时间毫秒数 - 超时毫秒数,得到过期时间 expired
226+
redis.call('zremrangebyscore', key, 0, expired)
227+
228+
-- 获取 zset 中的当前元素个数
229+
local current = tonumber(redis.call('zcard', key))
230+
local next = current + 1
231+
232+
if next > max then
233+
-- 达到限流大小 返回 0
234+
return 0;
235+
else
236+
-- 往 zset 中添加一个值、得分均为当前时间戳的元素,[value,score]
237+
redis.call("zadd", key, now, now)
238+
-- 每次访问均重新设置 zset 的过期时间,单位毫秒
239+
redis.call("pexpire", key, ttl)
240+
return next
241+
end
242+
```
243+
244+
### 1.5. 接口测试
245+
246+
```java
247+
/**
248+
* <p>
249+
* 测试
250+
* </p>
251+
*
252+
* @author yangkai.shen
253+
* @date Created in 2019/9/30 10:30
254+
*/
255+
@Slf4j
256+
@RestController
257+
public class TestController {
258+
259+
@RateLimiter(value = 5)
260+
@GetMapping("/test1")
261+
public Dict test1() {
262+
log.info("【test1】被执行了。。。。。");
263+
return Dict.create().set("msg", "hello,world!").set("description", "别想一直看到我,不信你快速刷新看看~");
264+
}
265+
266+
@GetMapping("/test2")
267+
public Dict test2() {
268+
log.info("【test2】被执行了。。。。。");
269+
return Dict.create().set("msg", "hello,world!").set("description", "我一直都在,卟离卟弃");
270+
}
271+
272+
@RateLimiter(value = 2, key = "测试自定义key")
273+
@GetMapping("/test3")
274+
public Dict test3() {
275+
log.info("【test3】被执行了。。。。。");
276+
return Dict.create().set("msg", "hello,world!").set("description", "别想一直看到我,不信你快速刷新看看~");
277+
}
278+
}
279+
```
280+
281+
### 1.6. 其余代码参见 demo
282+
283+
## 2. 测试
284+
285+
- 触发限流时控制台打印
286+
287+
![image-20190930155856711](assets/image-20190930155856711.png)
288+
289+
- 触发限流的时候 Redis 的数据
290+
291+
![image-20190930155735300](assets/image-20190930155735300.png)
292+
293+
## 3. 参考
294+
295+
- [mica-plus-redis 的分布式限流实现](https://github.com/lets-mica/mica/tree/master/mica-plus-redis)
296+
- [Java并发:分布式应用限流 Redis + Lua 实践](https://segmentfault.com/a/1190000016042927)
Loading
Loading

0 commit comments

Comments
 (0)