場景
爲了防止咱們的接口被人惡意訪問,好比有人經過JMeter工具頻繁訪問咱們的接口,致使接口響應變慢甚至崩潰,因此咱們須要對一些特定的接口進行IP限流,即必定時間內同一IP訪問的次數是有限的。html
實現原理
用Redis做爲限流組件的核心的原理,將用戶的IP地址當Key,一段時間內訪問次數爲value,同時設置該Key過時時間。前端
好比某接口設置相同IP10秒
內請求5次
,超過5次不讓訪問該接口。java
1. 第一次該IP地址存入redis的時候,key值爲IP地址,value值爲1,設置key值過時時間爲10秒。 2. 第二次該IP地址存入redis時,若是key沒有過時,那麼更新value爲2。 3. 以此類推當value已經爲5時,若是下次該IP地址在存入redis同時key尚未過時,那麼該Ip就不能訪問了。 4. 當10秒後,該key值過時,那麼該IP地址再進來,value又從1開始,過時時間仍是10秒,這樣反反覆覆。
說明
從上面的邏輯能夠看出,是一時間段內訪問次數受限,不是徹底不讓該IP訪問接口。git
技術框架
SpringBoot + RedisTemplate (採用自定義註解完成)github
這個能夠用於真實項目開發場景。redis
這邊採用自定義註解的目的就是,在接口上使用自定義註解,讓代碼看去很是整潔。spring
IpLimiterspringboot
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface IpLimiter { /** * 限流ip */ String ipAdress() ; /** * 單位時間限制經過請求數 */ long limit() default 10; /** * 單位時間,單位秒 */ long time() default 1; /** * 達到限流提示語 */ String message(); }
在接口上使用了自定義註解@IpLimiter
網絡
@Controller public class IpController { private static final Logger LOGGER = LoggerFactory.getLogger(IpController.class); private static final String MESSAGE = "請求失敗,你的IP訪問太頻繁"; //這裏就不獲取請求的ip,而是寫死一個IP @ResponseBody @RequestMapping("iplimiter") @IpLimiter(ipAdress = "127.198.66.01", limit = 5, time = 10, message = MESSAGE) public String sendPayment(HttpServletRequest request) throws Exception { return "請求成功"; } @ResponseBody @RequestMapping("iplimiter1") @IpLimiter(ipAdress = "127.188.145.54", limit = 4, time = 10, message = MESSAGE) public String sendPayment1(HttpServletRequest request) throws Exception { return "請求成功"; } }
這邊採用切面的方式處理自定義註解。同時爲了保證原子性,這邊寫了redis腳本ipLimiter.lua
來執行redis命令,來保證操做原子性。併發
@Aspect @Component public class IpLimterHandler { private static final Logger LOGGER = LoggerFactory.getLogger(IpLimterHandler.class); @Autowired RedisTemplate redisTemplate; /** * getRedisScript 讀取腳本工具類 * 這裏設置爲Long,是由於ipLimiter.lua 腳本返回的是數字類型 */ private DefaultRedisScript<Long> getRedisScript; @PostConstruct public void init() { getRedisScript = new DefaultRedisScript<>(); getRedisScript.setResultType(Long.class); getRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("ipLimiter.lua"))); LOGGER.info("IpLimterHandler[分佈式限流處理器]腳本加載完成"); } /** * 這個切點能夠不要,由於下面的自己就是個註解 */ // @Pointcut("@annotation(com.jincou.iplimiter.annotation.IpLimiter)") // public void rateLimiter() {} /** * 若是保留上面這個切點,那麼這裏能夠寫成 * @Around("rateLimiter()&&@annotation(ipLimiter)") */ @Around("@annotation(ipLimiter)") public Object around(ProceedingJoinPoint proceedingJoinPoint, IpLimiter ipLimiter) throws Throwable { if (LOGGER.isDebugEnabled()) { LOGGER.debug("IpLimterHandler[分佈式限流處理器]開始執行限流操做"); } Signature signature = proceedingJoinPoint.getSignature(); if (!(signature instanceof MethodSignature)) { throw new IllegalArgumentException("the Annotation @IpLimter must used on method!"); } /** * 獲取註解參數 */ // 限流模塊IP String limitIp = ipLimiter.ipAdress(); Preconditions.checkNotNull(limitIp); // 限流閾值 long limitTimes = ipLimiter.limit(); // 限流超時時間 long expireTime = ipLimiter.time(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("IpLimterHandler[分佈式限流處理器]參數值爲-limitTimes={},limitTimeout={}", limitTimes, expireTime); } // 限流提示語 String message = ipLimiter.message(); /** * 執行Lua腳本 */ List<String> ipList = new ArrayList(); // 設置key值爲註解中的值 ipList.add(limitIp); /** * 調用腳本並執行 */ Long result = (Long) redisTemplate.execute(getRedisScript, ipList, expireTime, limitTimes); if (result == 0) { String msg = "因爲超過單位時間=" + expireTime + "-容許的請求次數=" + limitTimes + "[觸發限流]"; LOGGER.debug(msg); // 達到限流返回給前端信息 return message; } if (LOGGER.isDebugEnabled()) { LOGGER.debug("IpLimterHandler[分佈式限流處理器]限流執行結果-result={},請求[正常]響應", result); } return proceedingJoinPoint.proceed(); } }
@Configuration public class RedisCacheConfig { private static final Logger LOGGER = LoggerFactory.getLogger(RedisCacheConfig.class); @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); //使用Jackson2JsonRedisSerializer來序列化和反序列化redis的value值(默認使用JDK的序列化方式) Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); serializer.setObjectMapper(mapper); template.setValueSerializer(serializer); //使用StringRedisSerializer來序列化和反序列化redis的key值 template.setKeySerializer(new StringRedisSerializer()); template.afterPropertiesSet(); LOGGER.info("Springboot RedisTemplate 加載完成"); return template; } }
優勢
減小網絡的開銷
: 腳本只執行一次,不須要發送屢次請求, 減小網絡傳輸;
保證原子操做
: 整個腳本做爲一個原子執行, 就不用擔憂併發問題;
--獲取KEY local key1 = KEYS[1] local val = redis.call('incr', key1) local ttl = redis.call('ttl', key1) --獲取ARGV內的參數並打印 local expire = ARGV[1] local times = ARGV[2] redis.log(redis.LOG_DEBUG,tostring(times)) redis.log(redis.LOG_DEBUG,tostring(expire)) redis.log(redis.LOG_NOTICE, "incr "..key1.." "..val); if val == 1 then redis.call('expire', key1, tonumber(expire)) else if ttl == -1 then redis.call('expire', key1, tonumber(expire)) end end if val > tonumber(times) then return 0 end return 1
#redis spring.redis.hostName= spring.redis.host= spring.redis.port=6379 spring.redis.jedis.pool.max-active=8 spring.redis.jedis.pool.max-wait= spring.redis.jedis.pool.max-idle=8 spring.redis.jedis.pool.min-idle=10 spring.redis.timeout=100ms spring.redis.password= logging.path= /Users/xub/log logging.level.com.jincou.iplimiter=DEBUG server.port=8888
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
完美
上面這個測試很是符合咱們的預期,前五次訪問接口是成功的,後面就失敗了,直到10秒後才能夠從新訪問,這樣反反覆覆。
其它的這邊就不一一展現了,附上該項目源碼。
Github地址
https://github.com/yudiandemingzi/ipLimiter
這個設計是我在刷github的時候看到確實很好,我這邊只是在它的基礎上作了點小改動,很是感謝該做者的分享。
github地址:https://github.com/TaXueWWL/shleld-ratelimter
有關AOP有篇文章講的不錯:spring aop 中@annotation()的使用
只要本身變優秀了,其餘的事情纔會跟着好起來(中將1)