利用redisson快速實(shí)現(xiàn)自定義限流注解(接口防刷)
問題:
在日常開發(fā)中,一些重要的對外接口,需要加上訪問頻率限制,以免造成資損失。
如登錄接口,當(dāng)用戶使用手機(jī)號(hào)+驗(yàn)證碼登錄時(shí),一般我們會(huì)生成6位數(shù)的隨機(jī)驗(yàn)證碼,并將驗(yàn)證碼有效期設(shè)置為1-3分鐘,如果對登錄接口不加以限制,理論上,通過技術(shù)手段,快速重試100000次,即可將驗(yàn)證碼窮舉出來。
解決思路:對登錄接口加上限流操作,如限制一分鐘內(nèi)最多登錄5次,登錄次數(shù)過多,就返回失敗提示,或者將賬號(hào)鎖定一段時(shí)間。
實(shí)現(xiàn)手段:利用redis的有序集合即Sorted Set數(shù)據(jù)結(jié)構(gòu),構(gòu)造一個(gè)令牌桶來實(shí)施限流。而redisson已經(jīng)幫我們封裝成了RRateLimiter,通過redisson,即可快速實(shí)現(xiàn)我們的目標(biāo)。
定義一個(gè)限流注解
import org.redisson.api.RateIntervalUnit; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface GlobalRateLimiter { String key(); long rate(); long rateInterval() default 1L; RateIntervalUnit rateIntervalUnit() default RateIntervalUnit.SECONDS; }
利用aop進(jìn)行切面
import com.zj.demoshow.annotion.GlobalRateLimiter; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.redisson.Redisson; import org.redisson.api.RRateLimiter; import org.redisson.api.RateIntervalUnit; import org.redisson.api.RateType; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.lang.reflect.Method; import java.util.concurrent.TimeUnit; @Aspect @Component @Slf4j public class GlobalRateLimiterAspect { @Resource private Redisson redisson; @Value("${spring.application.name}") private String applicationName; private final DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer(); @Pointcut(value = "@annotation(com.zj.demoshow.annotion.GlobalRateLimiter)") public void cut() { } @Around(value = "cut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); String className = method.getDeclaringClass().getName(); String methodName = method.getName(); GlobalRateLimiter globalRateLimiter = method.getDeclaredAnnotation(GlobalRateLimiter.class); Object[] params = joinPoint.getArgs(); long rate = globalRateLimiter.rate(); String key = globalRateLimiter.key(); long rateInterval = globalRateLimiter.rateInterval(); RateIntervalUnit rateIntervalUnit = globalRateLimiter.rateIntervalUnit(); if (key.contains("#")) { ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext ctx = new StandardEvaluationContext(); String[] parameterNames = discoverer.getParameterNames(method); if (parameterNames != null) { for (int i = 0; i < parameterNames.length; i++) { ctx.setVariable(parameterNames[i], params[i]); } } Expression expression = parser.parseExpression(key); Object value = expression.getValue(ctx); if (value == null) { throw new RuntimeException("key無效"); } key = value.toString(); } key = applicationName + "_" + className + "_" + methodName + "_" + key; log.info("設(shè)置限流鎖key={}", key); RRateLimiter rateLimiter = this.redisson.getRateLimiter(key); if (!rateLimiter.isExists()) { log.info("設(shè)置流量,rate={},rateInterval={},rateIntervalUnit={}", rate, rateInterval, rateIntervalUnit); rateLimiter.trySetRate(RateType.OVERALL, rate, rateInterval, rateIntervalUnit); //設(shè)置一個(gè)過期時(shí)間,避免key一直存在浪費(fèi)內(nèi)存,這里設(shè)置為延長5分鐘 long millis = rateIntervalUnit.toMillis(rateInterval); this.redisson.getBucket(key).expire(Long.sum(5 * 1000 * 60, millis), TimeUnit.MILLISECONDS); } boolean acquire = rateLimiter.tryAcquire(1); if (!acquire) { //這里直接拋出了異常 也可以拋出自定義異常,通過全局異常處理器攔截進(jìn)行一些其他邏輯的處理 throw new RuntimeException("請求頻率過高,此操作已被限制"); } return joinPoint.proceed(); } }
ok,通過以上兩步,即可完成我們的限流注解了,下面通過一個(gè)接口驗(yàn)證下效果。
新建一個(gè)controller,寫一個(gè)模擬登錄的方法。
@RestController @RequestMapping(value = "/user") public class UserController { @PostMapping(value = "/testForLogin") //以account為鎖的key,限制每分鐘最多登錄5次 @GlobalRateLimiter(key = "#params.account", rate = 5, rateInterval = 60) R<Object> testForLogin(@RequestBody @Validated LoginParams params) { //登錄邏輯 return R.success("登錄成功"); } }
啟動(dòng)服務(wù),通過postman訪問此接口進(jìn)行驗(yàn)證。
到此這篇關(guān)于利用redisson快速實(shí)現(xiàn)自定義限流注解的文章就介紹到這了,更多相關(guān)redisson自定義限流注解內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis節(jié)省內(nèi)存的十個(gè)技巧分享
你是否在工作中遇到過Redis的bigkey導(dǎo)致的內(nèi)存占用嚴(yán)重、查詢耗時(shí)大大增加?同時(shí)bigKey還可能導(dǎo)致Redis實(shí)例的崩潰,因?yàn)閮?nèi)存不夠用了,所以本文給大家介紹了Redis極大節(jié)省內(nèi)存的10個(gè)技巧,需要的朋友可以參考下2024-04-04Redis不是一直號(hào)稱單線程效率也很高嗎,為什么又采用多線程了?
這篇文章主要介紹了Redis不是一直號(hào)稱單線程效率也很高嗎,為什么又采用多線程了的相關(guān)資料,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03Redis設(shè)置密碼的實(shí)現(xiàn)步驟
本文主要介紹了Redis設(shè)置密碼的實(shí)現(xiàn)步驟,主要包括兩種方法:臨時(shí)密碼和持久密碼,具有一定的參考價(jià)值,感興趣的可以了解一下2023-08-08Redis 的過期策略與鍵的過期時(shí)間設(shè)置方法
Redis通過惰性刪除和定期刪除策略管理內(nèi)存,提供多種命令設(shè)置鍵的過期時(shí)間,并通過過期字典高效處理過期鍵,合理設(shè)置過期時(shí)間、監(jiān)控過期鍵數(shù)量和避免大量鍵同時(shí)過期是最佳實(shí)踐,本文介紹Redis 的過期策略與鍵的過期時(shí)間設(shè)置,感興趣的朋友一起看看吧2025-03-03