应用场景
生产环境中有很多业务场景需要限制API或网站的并发请求书、流量,网上也有很多成熟的框架、算法,但对于小型应用来说,可能引入框架、算法的代码比自己的业务代码都多了,得不偿失,性价比不高。所以简单的限流需求我们完全可以自己通过很少的代码实现。
原理介绍
不讲算法,直接大白话描述:主要是利用redis的incr
方法,给每个终端(通过IP判断、user-agent判断等等)设置一个redis的key,过期时间为T,然后每次请求就是用incr
加1,并重设这个key的过期时间为T。当这个key对应的value大于L后,判定超过了限流阈值,返回错误。其中,T是时间,L是最大流量,一句话描述就是在T时间内,最多只能访问L次。
限流的代码封装好后,通常是在springboot的Interceptor
拦截器中调用。
Redis限流步骤、代码
一、创建自定义注解
创建一个自定义注解,属性有:限流时间窗口、限流次数。当然,你也可以根据自己的业务,定义其他属性,比如限流类型(根据IP限流、根据user-agent限流、根据user限流等等)。代码如下:
package com.coderbbb.blogv2.config.anno;
import com.coderbbb.blogv2.config.mj.AccessLimitType;
import java.lang.annotation.*;
/**
* @Author: longge93
* @Date: 2021/6/5 22:41
*/
@Documented
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {
/**
* 时间窗口,单位秒
* @return
*/
int time() default 1;
/**
* 时间窗口内允许访问的最大次数
* @return
*/
int times() default 10;
}
二、创建限流service
在Springboot项目中新建一个AccessLimitService
的service,然后编写上面描述的限流算法。其中,使用到的RedisService
是作者自己封装的springboot操作redis的库,代码在文章《Springboot整合Redis和redis常用操作演示》中。
package com.coderbbb.blogv2.service;
import com.coderbbb.blogv2.config.BaseCommonConfig;
import com.coderbbb.blogv2.config.BaseRedisKeyConfig;
import com.coderbbb.blogv2.config.anno.AccessLimit;
import com.coderbbb.blogv2.config.mj.AccessLimitType;
import com.coderbbb.blogv2.database.dos.UserDO;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.method.HandlerMethod;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Locale;
/**
* @author longge93
*/
@Service
public class AccessLimitService {
@Autowired
private RedisService redisService;
public void check(HttpServletRequest request, HandlerMethod handler) throws Exception {
AccessLimit accessLimit = handler.getMethodAnnotation(AccessLimit.class);
if (accessLimit == null) {
return;
}
String ip = (String) request.getAttribute("ip");
String accessLimitKey = BaseRedisKeyConfig.ACCESS_LIMIT_KEY;
if (accessLimit.limitType() == AccessLimitType.LIMIT_BY_URL) {
accessLimitKey = accessLimitKey + DigestUtils.md5Hex(ip + request.getRequestURI());
} else {
accessLimitKey = accessLimitKey + DigestUtils.md5Hex(ip);
}
String v = redisService.getString(accessLimitKey);
redisService.incr(accessLimitKey);
if (v == null) {
redisService.setExpire(accessLimitKey, accessLimit.time());
return;
}
int vInt = Integer.parseInt(v);
if (vInt >= accessLimit.times()) {
//开始限流
throw new Exception("access limit exceed#" + request.getHeader("X-Forwarded-For"));
}
}
}
上面的限流算法中,是根据用户终端请求IP来限流的,你可以改造代码,来根据其他终端特征来限流。
三、在Interceptor拦截器中调用限流service
完成上面的步骤后,我们只要在自己的Interceptor
拦截器中调用上面的限流service即可,如果流量、并发超过限制,限流service会抛出异常,在拦截器中捕获异常,给用户返回对应的错误信息即可。代码如下:
@Component
public class BaseInterceptor implements HandlerInterceptor {
@Autowired
private AccessLimitService accessLimitService;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//真实IP
String ip = request.getHeader("X-Forwarded-For");
if (StringUtils.isEmpty(ip)) {
ip = "127.0.0.1";
}
request.setAttribute("ip", ip);
if ((handler instanceof HandlerMethod)) {
//限流
try {
accessLimitService.check(request, (HandlerMethod) handler);
} catch (Exception e) {
logger.error(e.getMessage() + "#" + urlFull);
response.setStatus(500);
return false;
}
}
return true;
}
}
其中,自定义的拦截器需要注册到springboot中才会正常工作,关于如何注册拦截器到springboot中,可以参考下图中的代码。
四、使用redis限流注解实现限流
在你需要限流的springboot controller method上,标记限流注解,然后多次调用该method,测试能否正常限流。使用代码截图如下: