利用redisson快速實現(xiàn)自定義限流注解(接口防刷)
問題:
在日常開發(fā)中,一些重要的對外接口,需要加上訪問頻率限制,以免造成資損失。
如登錄接口,當(dāng)用戶使用手機(jī)號+驗證碼登錄時,一般我們會生成6位數(shù)的隨機(jī)驗證碼,并將驗證碼有效期設(shè)置為1-3分鐘,如果對登錄接口不加以限制,理論上,通過技術(shù)手段,快速重試100000次,即可將驗證碼窮舉出來。
解決思路:對登錄接口加上限流操作,如限制一分鐘內(nèi)最多登錄5次,登錄次數(shù)過多,就返回失敗提示,或者將賬號鎖定一段時間。
實現(xiàn)手段:利用redis的有序集合即Sorted Set數(shù)據(jù)結(jié)構(gòu),構(gòu)造一個令牌桶來實施限流。而redisson已經(jīng)幫我們封裝成了RRateLimiter,通過redisson,即可快速實現(xiàn)我們的目標(biāo)。
定義一個限流注解
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è)置一個過期時間,避免key一直存在浪費內(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,通過以上兩步,即可完成我們的限流注解了,下面通過一個接口驗證下效果。
新建一個controller,寫一個模擬登錄的方法。
@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("登錄成功");
}
}啟動服務(wù),通過postman訪問此接口進(jìn)行驗證。

到此這篇關(guān)于利用redisson快速實現(xiàn)自定義限流注解的文章就介紹到這了,更多相關(guān)redisson自定義限流注解內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
用Redis實現(xiàn)微博關(guān)注關(guān)系
在微博中,每一個用戶都會有一個關(guān)注列表,一個粉絲列表。用戶可以查看自己的關(guān)注,粉絲列表,也可以查看別人的關(guān)注,粉絲列表。并且,要展示列表里每個人與當(dāng)前查看者的關(guān)注狀態(tài)。2015-09-09
Redis使用bloom-filter過濾器實現(xiàn)推薦去重
這篇文章主要介紹了Redis使用bloom-filter過濾器實現(xiàn)推薦去重,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-11-11
Redis+aop實現(xiàn)接口防刷(冪等)的解決方案
在高并發(fā)場景下,可能會因為網(wǎng)絡(luò)或者服務(wù)器原因,造成延遲,同時就是有可能會有人用腳本大量訪問你的接口,造成資源崩潰,所以本文給大家介紹了Redis+aop實現(xiàn)接口防刷(冪等)的解決方案,需要的朋友可以參考下2024-03-03
Redis動態(tài)熱點數(shù)據(jù)緩存策略設(shè)計
本文主要介紹了Redis動態(tài)熱點數(shù)據(jù)緩存策略設(shè)計,包括熱點數(shù)據(jù)識別、動態(tài)緩存、多級緩存、預(yù)加載機(jī)制、更新策略以及監(jiān)控告警等,具有一定的參考價值,感興趣的可以了解一下2025-01-01

