Redis

关注公众号 jb51net

关闭
首页 > 数据库 > Redis > redisson自定义限流注解

利用redisson快速实现自定义限流注解(接口防刷)

作者:二价亚铁

利用redis的有序集合即Sorted Set数据结构,构造一个令牌桶来实施限流,而redisson已经帮我们封装成了RRateLimiter,通过redisson,即可快速实现我们的目标,这篇文章主要介绍了利用redisson快速实现自定义限流注解,需要的朋友可以参考下

问题:
在日常开发中,一些重要的对外接口,需要加上访问频率限制,以免造成资损失。

如登录接口,当用户使用手机号+验证码登录时,一般我们会生成6位数的随机验证码,并将验证码有效期设置为1-3分钟,如果对登录接口不加以限制,理论上,通过技术手段,快速重试100000次,即可将验证码穷举出来。

解决思路:对登录接口加上限流操作,如限制一分钟内最多登录5次,登录次数过多,就返回失败提示,或者将账号锁定一段时间。

实现手段:利用redis的有序集合即Sorted Set数据结构,构造一个令牌桶来实施限流。而redisson已经帮我们封装成了RRateLimiter,通过redisson,即可快速实现我们的目标。

定义一个限流注解

  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进行切面

  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("设置限流锁key={}", key);
  		RRateLimiter rateLimiter = this.redisson.getRateLimiter(key);
  		if (!rateLimiter.isExists()) {
  			log.info("设置流量,rate={},rateInterval={},rateIntervalUnit={}", rate, rateInterval, rateIntervalUnit);
  			rateLimiter.trySetRate(RateType.OVERALL, rate, rateInterval, rateIntervalUnit);
  			//设置一个过期时间,避免key一直存在浪费内存,这里设置为延长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) {
  			//这里直接抛出了异常  也可以抛出自定义异常,通过全局异常处理器拦截进行一些其他逻辑的处理
  			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("登录成功");
	}
}

启动服务,通过postman访问此接口进行验证。

到此这篇关于利用redisson快速实现自定义限流注解的文章就介绍到这了,更多相关redisson自定义限流注解内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文