为何要限流
由于API接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机。限流(Rate limiting)指对应用服务的请求进行限制,例如某一接口的请求限制为100个每秒,对超过限制的请求则进行快速失败或丢弃。
限流可以应对:
热点业务带来的突发请求;
调用方bug导致的突发请求;
恶意攻击请求。
因此,对于公开的接口最好采取限流措施。
基于redis实现限流
添加pom
创建配置类
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Serializable> limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Serializable> template = new RedisTemplate<>();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
自定义限流注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Limit {
// 资源名称,用于描述接口功能
String name() default "";
// 资源 key
String key() default "";
// key prefix
String prefix() default "";
// 时间的,单位秒
int period();
// 限制访问次数
int count();
// 限制类型
LimitType limitType() default LimitType.CUSTOMER;
}
public enum LimitType {
/**
* 自定义key
*/
CUSTOMER,
/**
* 根据请求者IP
*/
IP;
}
创建限流切面类(根据自己业务需要可进行修改)
@Slf4j
@Aspect
@Component
public class LimitAspect {
private final RedisTemplate<String, Serializable> limitRedisTemplate;
@Autowired
public LimitAspect(RedisTemplate<String, Serializable> limitRedisTemplate) {
this.limitRedisTemplate = limitRedisTemplate;
}
@Pointcut("@annotation(com.bszn.managerplatform.common.Limit)")
public void pointcut() {
// do nothing
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Limit limitAnnotation = method.getAnnotation(Limit.class);
LimitType limitType = limitAnnotation.limitType();
String name = limitAnnotation.name();
String key;
String ip = getIpAddress();
int limitPeriod = limitAnnotation.period();
int limitCount = limitAnnotation.count();
switch (limitType) {
case IP:
key = ip;
break;
case CUSTOMER:
key = limitAnnotation.key();
break;
default:
key = StringUtils.upperCase(method.getName());
}
String argsname[] = ((CodeSignature) point.getSignature()).getParameterNames();
Map<String, Object> parammap = new HashMap<>();
if (argsname.length > 0) {
// 获取参数值
Object[] args = point.getArgs();
for(int i=0; i < argsname.length; i++) {
if(args[i] instanceof HttpServletRequest||args[i] instanceof HttpServletResponse ||args[i] instanceof MultipartFile){
continue;
}
if(Objects.equals("timestamp",argsname[i])){
continue;
}
if(Objects.equals("sign",argsname[i])){
continue;
}
}
}
ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limitAnnotation.prefix() + "_", key, ip,parammap));
String luaScript = buildLuaScript();
RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);
log.info("IP:{} 第 {} 次访问key为 {},描述为 [{}] 的接口", ip, count, keys, name);
if (count != null && count.intValue() <= limitCount) {
return point.proceed();
} else {
throw new BaseException(-1,"接口访问超出频率限制");
}
}
/**
* 限流脚本
* 调用的时候不超过阈值,则直接返回并执行计算器自加。
*
* @return lua脚本
*/
private String buildLuaScript() {
return "local c" +
"\nc = redis.call('get',KEYS[1])" +
"\nif c and tonumber(c) > tonumber(ARGV[1]) then" +
"\nreturn c;" +
"\nend" +
"\nc = redis.call('incr',KEYS[1])" +
"\nif tonumber(c) == 1 then" +
"\nredis.call('expire',KEYS[1],ARGV[2])" +
"\nend" +
"\nreturn c;";
}
private static final String UNKNOWN = "unknown";
public String getIpAddress() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
使用注解对接口进行限流