利用redisson快速實(shí)現(xiàn)自定義限流注解(接口防刷)
問(wèn)題:
在日常開(kāi)發(fā)中,一些重要的對(duì)外接口,需要加上訪問(wèn)頻率限制,以免造成資損失。
如登錄接口,當(dāng)用戶使用手機(jī)號(hào)+驗(yàn)證碼登錄時(shí),一般我們會(huì)生成6位數(shù)的隨機(jī)驗(yàn)證碼,并將驗(yàn)證碼有效期設(shè)置為1-3分鐘,如果對(duì)登錄接口不加以限制,理論上,通過(guò)技術(shù)手段,快速重試100000次,即可將驗(yàn)證碼窮舉出來(lái)。
解決思路:對(duì)登錄接口加上限流操作,如限制一分鐘內(nèi)最多登錄5次,登錄次數(shù)過(guò)多,就返回失敗提示,或者將賬號(hào)鎖定一段時(shí)間。
實(shí)現(xiàn)手段:利用redis的有序集合即Sorted Set數(shù)據(jù)結(jié)構(gòu),構(gòu)造一個(gè)令牌桶來(lái)實(shí)施限流。而redisson已經(jīng)幫我們封裝成了RRateLimiter,通過(guò)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無(wú)效"); } 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è)過(guò)期時(shí)間,避免key一直存在浪費(fèi)內(nèi)存,這里設(shè)置為延長(zhǎng)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) { //這里直接拋出了異常 也可以拋出自定義異常,通過(guò)全局異常處理器攔截進(jìn)行一些其他邏輯的處理 throw new RuntimeException("請(qǐng)求頻率過(guò)高,此操作已被限制"); } return joinPoint.proceed(); } }
ok,通過(guò)以上兩步,即可完成我們的限流注解了,下面通過(guò)一個(gè)接口驗(yàn)證下效果。
新建一個(gè)controller,寫(xiě)一個(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ù),通過(guò)postman訪問(wèn)此接口進(jìn)行驗(yàn)證。
到此這篇關(guān)于利用redisson快速實(shí)現(xiàn)自定義限流注解的文章就介紹到這了,更多相關(guān)redisson自定義限流注解內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis高并發(fā)超賣(mài)問(wèn)題解決方案圖文詳解
Redis是一種基于內(nèi)存的數(shù)據(jù)存儲(chǔ)系統(tǒng),被廣泛用于解決高并發(fā)問(wèn)題,下面這篇文章主要給大家介紹了關(guān)于Redis高并發(fā)超賣(mài)問(wèn)題解決方案的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-02-02如何在centos中安裝redis插件bloom-filter
布隆過(guò)濾器在第一次add的時(shí)候自動(dòng)創(chuàng)建基于默認(rèn)參數(shù)的過(guò)濾器,Redis還提供了自定義參數(shù)的布隆過(guò)濾器,下面這篇文章主要給大家介紹了關(guān)于如何在centos中安裝redis插件bloom-filter的相關(guān)資料,需要的朋友可以參考下2021-11-11如何提高Redis服務(wù)器的最大打開(kāi)文件數(shù)限制
文章討論了如何提高Redis服務(wù)器的最大打開(kāi)文件數(shù)限制,以支持高并發(fā)服務(wù),本文給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2025-01-01Redis如何使用zset處理排行榜和計(jì)數(shù)問(wèn)題
Redis的ZSET數(shù)據(jù)結(jié)構(gòu)非常適合處理排行榜和計(jì)數(shù)問(wèn)題,它可以在高并發(fā)的點(diǎn)贊業(yè)務(wù)中高效地管理點(diǎn)贊的排名,并且由于ZSET的排序特性,可以輕松實(shí)現(xiàn)根據(jù)點(diǎn)贊數(shù)實(shí)時(shí)排序的功能2025-02-02