├── .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 | ![https://img2018.cnblogs.com/blog/1090617/201906/1090617-20190605221148607-1621181354.gif](https://img2018.cnblogs.com/blog/1090617/201906/1090617-20190605221148607-1621181354.gif) 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 --------------------------------------------------------------------------------