├── .gitignore
├── README.md
├── pom.xml
└── src
└── main
├── java
└── com
│ └── oujiong
│ └── iplimiter
│ ├── Application.java
│ ├── annotation
│ └── IpLimiter.java
│ ├── controller
│ └── IpController.java
│ ├── handler
│ └── IpLimterHandler.java
│ └── redisconfig
│ └── RedisCacheConfig.java
└── resources
├── application.properties
└── ipLimiter.lua
/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | /target/
3 | !.mvn/wrapper/maven-wrapper.jar
4 |
5 | ### STS ###
6 | .apt_generated
7 | .classpath
8 | .factorypath
9 | .project
10 | .settings
11 | .springBeans
12 | .sts4-cache
13 |
14 | ### IntelliJ IDEA ###
15 | .idea
16 | *.iws
17 | *.iml
18 | *.ipr
19 |
20 | ### NetBeans ###
21 | /nbproject/private/
22 | /nbbuild/
23 | /dist/
24 | /nbdist/
25 | /.nb-gradle/
26 | /build/
27 |
28 | ### VS Code ###
29 | .vscode/
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 针对`该项目` 下面有博客详细的说明
2 |
3 | 1、[基于Redis组件的特性,实现一个分布式限流](https://www.cnblogs.com/qdhxhz/p/10982218.html)
4 |
5 | `场景`
6 |
7 | 为了防止我们的接口被人恶意访问,比如有人通过JMeter工具频繁访问我们的接口,导致接口响应变慢甚至崩溃,所以我们需要对一些特定的接口进行IP限流,即一定时间内同一IP访问的次数是有限的。
8 |
9 | `实现原理`
10 |
11 | 用Redis作为限流组件的核心的原理,将用户的IP地址当Key,一段时间内访问次数为value,同时设置该Key过期时间。
12 |
13 | 比如某接口设置`相同IP10秒`内请求`5次`,超过5次不让访问该接口。
14 |
15 | ```
16 | 1. 第一次该IP地址存入redis的时候,key值为IP地址,value值为1,设置key值过期时间为10秒。
17 | 2. 第二次该IP地址存入redis时,如果key没有过期,那么更新value为2。
18 | 3. 以此类推当value已经为5时,如果下次该IP地址在存入redis同时key还没有过期,那么该Ip就不能访问了。
19 | 4. 当10秒后,该key值过期,那么该IP地址再进来,value又从1开始,过期时间还是10秒,这样反反复复。
20 | ```
21 |
22 | `说明`
23 |
24 | 从上面的逻辑可以看出,是一时间段内访问次数受限,不是完全不让该IP永久访问接口。
25 |
26 | #### 技术架构
27 |
28 | 项目总体技术选型
29 |
30 | ```
31 | SpringBoot2.1.3 + Maven3.5.4 + Redis + lombok(插件)
32 | ```
33 |
34 | 同时这边是通过`自定义注解`实现 IP访问限流。所有采用 自定义注解+AOP方式实现。
35 |
36 | `测试`
37 |
38 | 
39 |
40 | 上面这个测试非常符合我们的预期,前五次访问接口是成功的,后面就失败了,直到10秒后才可以重新访问,这样反反复复。
41 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | org.springframework.boot
8 | spring-boot-starter-parent
9 | 2.1.3.RELEASE
10 |
11 | com.jincou
12 | iplimiter
13 | 0.0.1-SNAPSHOT
14 | iplimter
15 |
16 |
17 | UTF-8
18 | UTF-8
19 | 1.8
20 |
21 |
22 |
23 |
24 | org.springframework.boot
25 | spring-boot-starter-web
26 |
27 |
28 | org.springframework.boot
29 | spring-boot-starter
30 |
31 |
32 |
33 | org.springframework.boot
34 | spring-boot-starter-data-redis
35 | 2.1.5.RELEASE
36 |
37 |
38 |
39 | org.springframework.boot
40 | spring-boot-starter-aop
41 | 2.1.3.RELEASE
42 |
43 |
44 |
45 |
46 | org.apache.commons
47 | commons-lang3
48 | 3.8.1
49 |
50 |
51 |
52 | com.google.guava
53 | guava
54 | 27.0.1-jre
55 |
56 |
57 |
58 |
59 | com.fasterxml.jackson.core
60 | jackson-databind
61 | 2.9.8
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | org.springframework.boot
70 | spring-boot-maven-plugin
71 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/src/main/java/com/oujiong/iplimiter/Application.java:
--------------------------------------------------------------------------------
1 | package com.oujiong.iplimiter;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class Application {
8 |
9 | public static void main(String[] args) {
10 | SpringApplication.run(Application.class, args);
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/com/oujiong/iplimiter/annotation/IpLimiter.java:
--------------------------------------------------------------------------------
1 | package com.oujiong.iplimiter.annotation;
2 |
3 | import java.lang.annotation.*;
4 |
5 | /**
6 | * @Description: IP限流注解*
7 | * @author xub
8 | * @date 2019/6/4 下午10:20
9 | */
10 | @Target(ElementType.METHOD)
11 | @Retention(RetentionPolicy.RUNTIME)
12 | @Documented
13 | public @interface IpLimiter {
14 |
15 | /**
16 | * 限流ip
17 | */
18 | String ipAdress() ;
19 | /**
20 | * 单位时间限制通过请求数
21 | */
22 | long limit() default 10;
23 |
24 | /**
25 | * 单位时间,单位秒
26 | */
27 | long time() default 1;
28 |
29 | /**
30 | * 达到限流提示语
31 | */
32 | String message();
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/com/oujiong/iplimiter/controller/IpController.java:
--------------------------------------------------------------------------------
1 | package com.oujiong.iplimiter.controller;
2 |
3 | import com.oujiong.iplimiter.annotation.IpLimiter;
4 | import org.slf4j.Logger;
5 | import org.slf4j.LoggerFactory;
6 | import org.springframework.stereotype.Controller;
7 | import org.springframework.web.bind.annotation.RequestMapping;
8 | import org.springframework.web.bind.annotation.ResponseBody;
9 |
10 | import javax.servlet.http.HttpServletRequest;
11 |
12 | /**
13 | * @Description: 接口测试
14 | *
15 | * @author xub
16 | * @date 2019/6/4 上午9:10
17 | */
18 | @Controller
19 | public class IpController {
20 |
21 | private static final Logger LOGGER = LoggerFactory.getLogger(IpController.class);
22 | private static final String MESSAGE = "请求失败,你的IP访问太频繁";
23 |
24 | //这里就不获取请求的ip,而是写死一个IP
25 | @ResponseBody
26 | @RequestMapping("iplimiter")
27 | @IpLimiter(ipAdress = "127.198.66.01", limit = 5, time = 10, message = MESSAGE)
28 | public String sendPayment(HttpServletRequest request) throws Exception {
29 | return "请求成功";
30 | }
31 | @ResponseBody
32 | @RequestMapping("iplimiter1")
33 | @IpLimiter(ipAdress = "127.188.145.54", limit = 4, time = 10, message = MESSAGE)
34 | public String sendPayment1(HttpServletRequest request) throws Exception {
35 | return "请求成功";
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/com/oujiong/iplimiter/handler/IpLimterHandler.java:
--------------------------------------------------------------------------------
1 | package com.oujiong.iplimiter.handler;
2 |
3 | import com.google.common.base.Preconditions;
4 | import com.oujiong.iplimiter.annotation.IpLimiter;
5 | import org.aspectj.lang.ProceedingJoinPoint;
6 | import org.aspectj.lang.Signature;
7 | import org.aspectj.lang.annotation.Around;
8 | import org.aspectj.lang.annotation.Aspect;
9 | import org.aspectj.lang.reflect.MethodSignature;
10 | import org.slf4j.Logger;
11 | import org.slf4j.LoggerFactory;
12 | import org.springframework.beans.factory.annotation.Autowired;
13 | import org.springframework.core.io.ClassPathResource;
14 | import org.springframework.data.redis.core.RedisTemplate;
15 | import org.springframework.data.redis.core.script.DefaultRedisScript;
16 | import org.springframework.scripting.support.ResourceScriptSource;
17 | import org.springframework.stereotype.Component;
18 |
19 | import javax.annotation.PostConstruct;
20 | import java.util.ArrayList;
21 | import java.util.List;
22 |
23 | /**
24 | * @Description: 限流处理器
25 | *
26 | * @author xub
27 | * @date 2019/10/27 1:17
28 | */
29 | @Aspect
30 | @Component
31 | public class IpLimterHandler {
32 |
33 | private static final Logger LOGGER = LoggerFactory.getLogger(IpLimterHandler.class);
34 |
35 | @Autowired
36 | RedisTemplate redisTemplate;
37 |
38 | /**
39 | * getRedisScript 读取脚本工具类
40 | * 这里设置为Long,是因为ipLimiter.lua 脚本返回的是数字类型
41 | */
42 | private DefaultRedisScript getRedisScript;
43 |
44 | @PostConstruct
45 | public void init() {
46 | getRedisScript = new DefaultRedisScript<>();
47 | getRedisScript.setResultType(Long.class);
48 | getRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("ipLimiter.lua")));
49 | LOGGER.info("IpLimterHandler[分布式限流处理器]脚本加载完成");
50 | }
51 |
52 | /**
53 | * 这个切点可以不要,因为下面的本身就是个注解
54 | */
55 | // @Pointcut("@annotation(IpLimiter)")
56 | // public void rateLimiter() {}
57 |
58 | /**
59 | * 如果保留上面这个切点,那么这里可以写成
60 | * @Around("rateLimiter()&&@annotation(ipLimiter)")
61 | */
62 | @Around("@annotation(ipLimiter)")
63 | public Object around(ProceedingJoinPoint proceedingJoinPoint, IpLimiter ipLimiter) throws Throwable {
64 | if (LOGGER.isDebugEnabled()) {
65 | LOGGER.debug("IpLimterHandler[分布式限流处理器]开始执行限流操作");
66 | }
67 | Signature signature = proceedingJoinPoint.getSignature();
68 | if (!(signature instanceof MethodSignature)) {
69 | throw new IllegalArgumentException("the Annotation @IpLimter must used on method!");
70 | }
71 | /**
72 | * 获取注解参数
73 | */
74 | // 限流模块IP
75 | String limitIp = ipLimiter.ipAdress();
76 | Preconditions.checkNotNull(limitIp);
77 | // 限流阈值
78 | long limitTimes = ipLimiter.limit();
79 | // 限流超时时间
80 | long expireTime = ipLimiter.time();
81 | if (LOGGER.isDebugEnabled()) {
82 | LOGGER.debug("IpLimterHandler[分布式限流处理器]参数值为-limitTimes={},limitTimeout={}", limitTimes, expireTime);
83 | }
84 | // 限流提示语
85 | String message = ipLimiter.message();
86 | /**
87 | * 执行Lua脚本
88 | */
89 | List ipList = new ArrayList();
90 | // 设置key值为注解中的值
91 | ipList.add(limitIp);
92 | /**
93 | * 调用脚本并执行
94 | */
95 | Long result = (Long) redisTemplate.execute(getRedisScript, ipList, expireTime, limitTimes);
96 | if (result == 0) {
97 | String msg = "由于超过单位时间=" + expireTime + "-允许的请求次数=" + limitTimes + "[触发限流]";
98 | LOGGER.debug(msg);
99 | // 达到限流返回给前端信息
100 | return message;
101 | }
102 | if (LOGGER.isDebugEnabled()) {
103 | LOGGER.debug("IpLimterHandler[分布式限流处理器]限流执行结果-result={},请求[正常]响应", result);
104 | }
105 | return proceedingJoinPoint.proceed();
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/main/java/com/oujiong/iplimiter/redisconfig/RedisCacheConfig.java:
--------------------------------------------------------------------------------
1 | package com.oujiong.iplimiter.redisconfig;
2 |
3 | import com.fasterxml.jackson.annotation.JsonAutoDetect;
4 | import com.fasterxml.jackson.annotation.PropertyAccessor;
5 | import com.fasterxml.jackson.databind.ObjectMapper;
6 | import org.slf4j.Logger;
7 | import org.slf4j.LoggerFactory;
8 | import org.springframework.context.annotation.Bean;
9 | import org.springframework.context.annotation.Configuration;
10 | import org.springframework.data.redis.connection.RedisConnectionFactory;
11 | import org.springframework.data.redis.core.RedisTemplate;
12 | import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
13 | import org.springframework.data.redis.serializer.StringRedisSerializer;
14 |
15 | /**
16 | * @Description: config
17 | *
18 | * @author xub
19 | * @date 2019/6/4 下午10:53
20 | */
21 | @Configuration
22 | public class RedisCacheConfig {
23 |
24 | private static final Logger LOGGER = LoggerFactory.getLogger(RedisCacheConfig.class);
25 |
26 | @Bean
27 | public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
28 | RedisTemplate template = new RedisTemplate<>();
29 | template.setConnectionFactory(factory);
30 |
31 | //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
32 | Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
33 |
34 | ObjectMapper mapper = new ObjectMapper();
35 | mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
36 | mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
37 | serializer.setObjectMapper(mapper);
38 |
39 | template.setValueSerializer(serializer);
40 | //使用StringRedisSerializer来序列化和反序列化redis的key值
41 | template.setKeySerializer(new StringRedisSerializer());
42 | template.afterPropertiesSet();
43 | LOGGER.info("Springboot RedisTemplate 加载完成");
44 | return template;
45 | }
46 | }
--------------------------------------------------------------------------------
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | #redis
2 | spring.redis.hostName=
3 | spring.redis.host=
4 | spring.redis.port=6379
5 | spring.redis.jedis.pool.max-active=8
6 | spring.redis.jedis.pool.max-wait=
7 | spring.redis.jedis.pool.max-idle=8
8 | spring.redis.jedis.pool.min-idle=10
9 | spring.redis.timeout=1000ms
10 | #spring.redis.password=
11 |
12 | logging.path= /Users/xub/log
13 | logging.level.com.jincou.iplimiter=DEBUG
14 | server.port=8888
15 |
--------------------------------------------------------------------------------
/src/main/resources/ipLimiter.lua:
--------------------------------------------------------------------------------
1 | --获取KEY
2 | local key1 = KEYS[1]
3 |
4 | local val = redis.call('incr', key1)
5 | local ttl = redis.call('ttl', key1)
6 |
7 | --获取ARGV内的参数并打印
8 | local expire = ARGV[1]
9 | local times = ARGV[2]
10 |
11 | redis.log(redis.LOG_DEBUG,tostring(times))
12 | redis.log(redis.LOG_DEBUG,tostring(expire))
13 |
14 | redis.log(redis.LOG_NOTICE, "incr "..key1.." "..val);
15 | if val == 1 then
16 | redis.call('expire', key1, tonumber(expire))
17 | else
18 | if ttl == -1 then
19 | redis.call('expire', key1, tonumber(expire))
20 | end
21 | end
22 |
23 | if val > tonumber(times) then
24 | return 0
25 | end
26 | return 1
--------------------------------------------------------------------------------