SpringBoot+Redis+Lua分布式限流的實現(xiàn)
Redis支持LUA腳本的主要優(yōu)勢
LUA腳本的融合將使Redis數(shù)據(jù)庫產(chǎn)生更多的使用場景,迸發(fā)更多新的優(yōu)勢:
- 高效性:減少網(wǎng)絡(luò)開銷及時延,多次redis服務(wù)器網(wǎng)絡(luò)請求的操作,使用LUA腳本可以用一個請求完成
- 數(shù)據(jù)可靠性:Redis會將整個腳本作為一個整體執(zhí)行,中間不會被其他命令插入。
- 復(fù)用性:LUA腳本執(zhí)行后會永久存儲在Redis服務(wù)器端,其他客戶端可以直接復(fù)用
- 可嵌入性:可嵌入JAVA,C#等多種編程語言,支持不同操作系統(tǒng)跨平臺交互
- 簡單強大:小巧輕便,資源占用率低,支持過程化和對象化的編程語言
自己也是第一次在工作中使用lua這種語言,記錄一下
創(chuàng)建Lua文件req_ratelimit.lua
local key = KEYS[1] --限流KEY local limitCount = tonumber(ARGV[1]) --限流大小 local limitTime = tonumber(ARGV[2]) --限流時間 local current = redis.call('get', key); if current then if current + 1 > limitCount then --如果超出限流大小 return 0 else redis.call("INCRBY", key,"1") return current + 1 end else redis.call("set", key,"1") redis.call("expire", key,limitTime) return 1 end
自定義注解RateLimiter
package com.shinedata.ann; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimiter { /** * 限流唯一標(biāo)識 * @return */ String key() default "rate.limit:"; /** * 限流時間 * @return */ int time() default 1; /** * 限流次數(shù) * @return */ int count() default 100; /** *是否限制IP,默認 否 * @return */ boolean restrictionsIp() default false; }
定義切面RateLimiterAspect
package com.shinedata.aop; import com.shinedata.ann.RateLimiter; import com.shinedata.config.redis.RedisUtils; import com.shinedata.exception.RateLimiterException; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.scripting.support.ResourceScriptSource; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.annotation.PostConstruct; import javax.servlet.http.HttpServletRequest; import java.io.Serializable; import java.lang.reflect.Method; import java.util.Collections; import java.util.List; /** * @ClassName RateLimiterAspect * @Author yupanpan * @Date 2020/5/6 13:46 */ @Aspect @Component public class RateLimiterAspect { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private static ThreadLocal<String> ipThreadLocal=new ThreadLocal(); private DefaultRedisScript<Number> redisScript; @PostConstruct public void init(){ redisScript = new DefaultRedisScript<Number>(); redisScript.setResultType(Number.class); redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/req_ratelimit.lua"))); } @Around("@annotation(com.shinedata.ann.RateLimiter)") public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable { try { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); Class<?> targetClass = method.getDeclaringClass(); RateLimiter rateLimit = method.getAnnotation(RateLimiter.class); if (rateLimit != null) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); boolean restrictionsIp = rateLimit.restrictionsIp(); if(restrictionsIp){ ipThreadLocal.set(getIpAddr(request)); } StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append(rateLimit.key()); if(StringUtils.isNotBlank(ipThreadLocal.get())){ stringBuffer.append(ipThreadLocal.get()).append("-"); } stringBuffer.append("-").append(targetClass.getName()).append("- ").append(method.getName()); List<String> keys = Collections.singletonList(stringBuffer.toString()); Number number = RedisUtils.execute(redisScript, keys, rateLimit.count(), rateLimit.time()); if (number != null && number.intValue() != 0 && number.intValue() <= rateLimit.count()) { logger.info("限流時間段內(nèi)訪問第:{} 次", number.toString()); return joinPoint.proceed(); }else { logger.error("已經(jīng)到設(shè)置限流次數(shù),當(dāng)前次數(shù):{}",number.toString()); throw new RateLimiterException("服務(wù)器繁忙,請稍后再試"); } } else { return joinPoint.proceed(); } }finally { ipThreadLocal.remove(); } } public static String getIpAddr(HttpServletRequest request) { String ipAddress = null; try { ipAddress = request.getHeader("x-forwarded-for"); if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("Proxy-Client-IP"); } if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("WL-Proxy-Client-IP"); } if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getRemoteAddr(); } // 對于通過多個代理的情況,第一個IP為客戶端真實IP,多個IP按照','分割 if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()= 15 if (ipAddress.indexOf(",") > 0) { ipAddress = ipAddress.substring(0, ipAddress.indexOf(",")); } } } catch (Exception e) { ipAddress = ""; } return ipAddress; } }
Spring data redis提供了DefaultRedisScript來使用lua和redis進行交互,具體的詳情網(wǎng)上很多文章,這里使用ThreadLocal是因為IP存在可變的,保證自己的線程的IP不會被其他線程所修改,切記要最后清理ThreadLocal,防止內(nèi)存泄漏
RedisUtils工具類(方法太多,只展示execute方法)
package com.shinedata.config.redis; import org.checkerframework.checker.units.qual.K; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.script.RedisScript; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import javax.annotation.PostConstruct; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; /** * @ClassName RedisUtils * @Author yupanpan * @Date 2019/11/20 13:38 */ @Component public class RedisUtils { @Autowired @Qualifier("redisTemplate") private RedisTemplate<String, Object> redisTemplate; private static RedisUtils redisUtils; @PostConstruct public void init() { redisUtils = this; redisUtils.redisTemplate = this.redisTemplate; } public static Number execute(DefaultRedisScript<Number> script, List keys, Object... args) { return redisUtils.redisTemplate.execute(script, keys,args); } }
自己配置的RedisTemplate
package com.shinedata.config.redis; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import redis.clients.jedis.JedisPoolConfig; /** * @ClassName RedisConfig * @Author yupanpan * @Date 2019/11/20 13:26 */ @Configuration public class RedisConfig extends RedisProperties{ protected Logger log = LogManager.getLogger(RedisConfig.class); /** * JedisPoolConfig 連接池 * @return */ @Bean("jedisPoolConfig") public JedisPoolConfig jedisPoolConfig() { JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); // 最大空閑數(shù) jedisPoolConfig.setMaxIdle(500); jedisPoolConfig.setMinIdle(100); // 連接池的最大數(shù)據(jù)庫連接數(shù) jedisPoolConfig.setMaxTotal(6000); // 最大建立連接等待時間 jedisPoolConfig.setMaxWaitMillis(5000); // 逐出連接的最小空閑時間 默認1800000毫秒(30分鐘) jedisPoolConfig.setMinEvictableIdleTimeMillis(100); // 每次逐出檢查時 逐出的最大數(shù)目 如果為負數(shù)就是 : 1/abs(n), 默認3 // jedisPoolConfig.setNumTestsPerEvictionRun(numTestsPerEvictionRun); // 逐出掃描的時間間隔(毫秒) 如果為負數(shù),則不運行逐出線程, 默認-1 jedisPoolConfig.setTimeBetweenEvictionRunsMillis(600); // 是否在從池中取出連接前進行檢驗,如果檢驗失敗,則從池中去除連接并嘗試取出另一個 jedisPoolConfig.setTestOnBorrow(true); // 在空閑時檢查有效性, 默認false jedisPoolConfig.setTestWhileIdle(false); return jedisPoolConfig; } /** * JedisConnectionFactory * @param jedisPoolConfig */ @Bean("jedisConnectionFactory") public JedisConnectionFactory jedisConnectionFactory(@Qualifier("jedisPoolConfig")JedisPoolConfig jedisPoolConfig) { JedisConnectionFactory JedisConnectionFactory = new JedisConnectionFactory(jedisPoolConfig); // 連接池 JedisConnectionFactory.setPoolConfig(jedisPoolConfig); // IP地址 JedisConnectionFactory.setHostName(redisHost); // 端口號 JedisConnectionFactory.setPort(redisPort); // 如果Redis設(shè)置有密碼 JedisConnectionFactory.setPassword(redisPassword); // 客戶端超時時間單位是毫秒 JedisConnectionFactory.setTimeout(10000); return JedisConnectionFactory; } /** * 實例化 RedisTemplate 對象代替原有的RedisTemplate<String, String> * @return */ @Bean("redisTemplate") public RedisTemplate<String, Object> functionDomainRedisTemplate(@Qualifier("jedisConnectionFactory") RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); initDomainRedisTemplate(redisTemplate, redisConnectionFactory); return redisTemplate; } /** * 設(shè)置數(shù)據(jù)存入 redis 的序列化方式 * @param redisTemplate * @param factory */ private void initDomainRedisTemplate(RedisTemplate<String, Object> redisTemplate, RedisConnectionFactory factory) { // 如果不配置Serializer,那么存儲的時候缺省使用String,比如如果用User類型存儲,那么會提示錯誤User can't cast // to String! redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // 開啟事務(wù)/true必須手動釋放連接,false會自動釋放連接 如果調(diào)用方有用@Transactional做事務(wù)控制,可以開啟事務(wù),Spring會處理連接問題 redisTemplate.setEnableTransactionSupport(false); redisTemplate.setConnectionFactory(factory); } }
全局Controller異常處理GlobalExceptionHandler
package com.shinedata.exception; import com.fasterxml.jackson.databind.JsonMappingException; import com.shinedata.util.ResultData; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class GlobalExceptionHandler { private Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); @ExceptionHandler(value = RateLimiterException.class) @ResponseStatus(HttpStatus.OK) public ResultData runtimeExceptionHandler(RateLimiterException e) { logger.error("系統(tǒng)錯誤:", e); return ResultData.getResultError(StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : "處理失敗"); } @ExceptionHandler(value = Exception.class) @ResponseStatus(HttpStatus.OK) public ResultData runtimeExceptionHandler(RuntimeException e) { Throwable cause = e.getCause(); logger.error("系統(tǒng)錯誤:", e); logger.error(e.getMessage()); if (cause instanceof JsonMappingException) { return ResultData.getResultError("參數(shù)錯誤"); } return ResultData.getResultError(StringUtils.isNotBlank(e.getMessage()) ? e.getMessage() : "處理失敗"); } }
使用就很簡單了,一個注解搞定
補充:優(yōu)化了lua為
local key = KEYS[1] local limitCount = tonumber(ARGV[1]) local limitTime = tonumber(ARGV[2]) local current = redis.call('get', key); if current then redis.call("INCRBY", key,"1") return current + 1 else redis.call("set", key,"1") redis.call("expire", key,limitTime) return 1 end
到此這篇關(guān)于SpringBoot+Redis+Lua分布式限流的實現(xiàn)的文章就介紹到這了,更多相關(guān)SpringBoot Redis Lua分布式限流內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JDK13.0.1安裝與環(huán)境變量的配置教程圖文詳解(Win10平臺為例)
這篇文章主要介紹了JDK13.0.1安裝與環(huán)境變量的配置教程圖文詳解(Win10平臺為例),本文圖文并茂給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2020-01-01SpringBoot使用Scheduling實現(xiàn)定時任務(wù)的示例代碼
Spring Boot提供了一種方便的方式來實現(xiàn)定時任務(wù),即使用Spring的@Scheduled注解,通過在方法上添加@Scheduled注解,我們可以指定方法在何時執(zhí)行,本文我們就給大家介紹一下SpringBoot如何使用Scheduling實現(xiàn)定時任務(wù),需要的朋友可以參考下2023-08-08springboot配置mysql數(shù)據(jù)庫spring.datasource.url報錯的解決
這篇文章主要介紹了springboot配置mysql數(shù)據(jù)庫spring.datasource.url報錯的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-01-01SpringBoot2.0 整合 Dubbo框架實現(xiàn)RPC服務(wù)遠程調(diào)用方法
這篇文章主要介紹了SpringBoot2.0 整合 Dubbo框架 實現(xiàn)RPC服務(wù)遠程調(diào)用 ,本文給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-07-07Java統(tǒng)計一個字符串在另外一個字符串出現(xiàn)次數(shù)的方法
這篇文章主要介紹了Java統(tǒng)計一個字符串在另外一個字符串出現(xiàn)次數(shù)的方法,涉及java字符串遍歷、正則匹配等相關(guān)操作技巧,需要的朋友可以參考下2018-03-03Java servlet 使用 PrintWriter 時的編碼與亂碼的示例代碼
本篇文章主要介紹了Java servlet 使用 PrintWriter 時的編碼與亂碼的示例代碼,探討了 PrintWriter 的缺省編碼與普通字符流的缺省編碼的差異,具有一定的參考價值,有興趣的可以了解一下2017-11-11Springboot集成Ehcache3實現(xiàn)本地緩存的配置方法
EhCache是一個純Java的進程內(nèi)緩存框架,是 Hibernate 中默認的 CacheProvider,同Redis一樣,EhCache 不是純內(nèi)存緩存,它支持基于內(nèi)存和磁盤的二級緩存,本文介紹Springboot集成Ehcache3實現(xiàn)本地緩存的配置方法,感興趣的朋友一起看看吧2024-04-04